Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Le Tutoriel

C mieux


melem.developpez.com :: Initiation au langage C

Bien débuter en Langage C

Table des matières

  • I. Qu'est-ce que le C ?
  • II. Je débute :
    • II-A. Quels livres/tutoriels en ligne puis-je lire ?
    • II-B. Quels livres puis-je lire ?
    • II-C. Quels sont les outils dont j'ai besoin ?
  • III. Je veux approfondir mes connaissances :
    • III-A. Quels livres/tutoriels en ligne puis-je lire ?
    • III-B. Quels livres puis-je lire ?
    • III-C. Quels outils supplémentaires sont à ma disposition ?
  • IV. Où puis-je trouver des exercices à faire ?
  • V. Je veux aller plus loin :
    • V-A. Comment créer des applications graphiques ?
      • V-A-a. GTK+ (la GUI portable) :
      • V-A-b. L'API Win32 (uniquement pour Windows) :
    • V-B. Comment créer des jeux ?
      • V-B-a. La 2D :
      • V-B-b. La 3D :
  • VI. J'ai tout lu mais il y a des choses que je n'ai pas compris, que faire ?
  • VII. Conclusion
  • VIII. Remerciements

I. Qu'est-ce que le C ?

Créé au début des années 70 par Dennis Ritchie et Brian Kernighan, le Langage C reste encore aujourd'hui un des langages les plus utilisé au monde !

Ce langage de programmation est à la base des systèmes d'exploitations que nous connaissons aujourd'hui ou au moins du noyau de ces systèmes comme par exemple Unix/Linux. Le Langage C a justement été créé pour un seul et unique but au départ, développer un système d'exploitation (Unix) mais au fil du temps, grâce à sa puissance, il a été adopté par une large communauté de développeurs ce qui a permis au langage d'évoluer et surtout d'être standardisé.

Ce langage est multi plate-forme, c'est-à-dire qu'un programme que vous créez par exemple sous Linux pourrais tout à fait être re-compilé sous Windows, BeOS, Amiga, etc.... sans devoir changer grand chose (voir rien du tout) dans le code source si vous respectez les normes en vigueur ANSI/ISO.

II. Je débute :

Les débuts ne sont jamais très aisés, notamment en ce qui concerne le choix des livres, la recherche de tutoriels en ligne et dans le choix des éditeurs ou EDI (Environnement de Développement Intégré). Les catégories ci-dessous vous permettrons de vous acquitter de cette tâche souvent très longue et désagréable pour les débutants, tout ce que vous avez à faire, c'est de suivre ces différentes parties !

II-A. Quels livres/tutoriels en ligne puis-je lire ?

  • Liste des cours complets sélectionnés par developpez.com
  • Initiation au Langage C - Emmanuel Delahaye
  • Codage en Langage C - Emmanuel Delahaye
  • Initiation au langage C - Jessee Michaël C. Edouard

II-B. Quels livres puis-je lire ?

  • Méthodologie de la programmation en C

II-C. Quels sont les outils dont j'ai besoin ?

Il vous faut pour bien commencer, un EDI (Environnement de Développement Intégré). Plusieurs EDI sont à votre disposition mais, la plupart d'entre eux ne sont disponibles que pour un seul système d'exploitation.

C'est là que Code::Blocks se distingue des autres EDI car il est portable, ce qui signifie qu'il est disponible sur au moins les systèmes les plus utilisés et plus connus soit Windows, Linux et Mac OS X.

Ci-dessous vous trouverez un lien direct de téléchargement de Code::Blocks pour chacun de ces systèmes:

Image personnelle Sous Windows, vous avez la possibilité d'utiliser la version dite "Stable" ce qui est souvent préférable: Téléchargement de la version stable: 12.11
Cette version est livrée avec la suite de compilation MingW !
Image personnelle Si votre système est un système Linux, vous avez alors le choix entre différents package selon votre distribution: Téléchargement des versions 12.11 pour Linux A noter également que sur les distributions Debian et Ubuntu vous pouvez télécharger et installer ce programme avec la commande apt-get install à partir de votre console.
Image personnelle Voici la version 8.02 si vous disposez de Mac OS X: Téléchargement de la version Mac

Vous pouvez également utiliser les versions en cours de développement, ca ne reste que des versions beta donc plus ou moins stables: Nightly Builds

Pour l'installation de Code::Blocks vous pouvez consulter les tutoriels suivants: Guides d'installation de Code::Blocks (Windows - Linux)

Voici également une présentation et test de cet EDI : Test de l'EDI C/C++ Code::Blocks

III. Je veux approfondir mes connaissances :

Vous avez acquis de bonnes bases en Langage C, vous désirez approfondir vos connaissances soit dans le C ANSI/ISO ou voir les normes POSIX ou encore vous plonger dans la programmation spécifique à Linux ? Les livres et tutoriels ci-dessous vous sont proposés par developpez.com !

III-A. Quels livres/tutoriels en ligne puis-je lire ?

  • Liste des tutoriels hébergés sur developpez.com
  • Notes sur le Langage C - Emmanuel Delahaye
  • Concevoir un composant logiciel en C - Emmanuel Delahaye
  • Les types abstraits de données (ADT) - Emmanuel Delahaye
  • Notes sur les réseaux - Emmanuel Delahaye
  • Les Threads POSIX.1 - Emmanuel Delahaye
  • Comment se fabriquer des entrées solides en C - Emmanuel Delahaye

III-B. Quels livres puis-je lire ?

  • Le langage C - Norme ANSI, 2ème édition
  • Exercices corrigés sur le langage C - Solutions des exercices du manuel de langage C de Kerrighan et Ritchie
  • C en action
  • Maîtrise des algorithmes en C
  • Programmation système en C sous Linux : Signaux, processus, threads, IPC et sockets

III-C. Quels outils supplémentaires sont à ma disposition ?

Dans cette section vous sont proposés des outils de débogage (pratique indispensable pour avoir des applications solides et avec le moins de failles et bugs possibles), des outils pour détecter des fuites mémoire, etc... Certains d'entre eux ne sont disponibles que sous un système, ils sont donc classés par OS :

Image personnelle Le debogueur phare de Microsoft et sans doute le meilleur sur ce système d'exploitation reste encore celui qui est fournit par défaut dans les solutions Visual Studio. La seule chose qui change ici, c'est que pour l'utiliser, il vous faut utiliser également l'éditeur Visual C++ (ici Visual C++ 2010 Express Edition) qu'on peut télécharger gratuitement sur la msdn: Télécharger et installer Visual C++ 2010

Sur le lien ci-contre vous pouvez voir les étapes pour son installation Microsoft Visual C++ 2010 Express : Téléchargement, installation et configuration
Voici également un tutoriel vous permettant d'apprendre à utiliser ce debogueur: Utiliser efficacement le débogueur de Visual Studio
Image personnelle Sous Linux vous avez l'embarat du choix, voici sans doute le meilleur que l'on puisse faire à l'heure actuelle tout en restant le maximum dans la simplicité !

DDD :
Cet outil permet le débogage de vos applications. Ici plusieures solutions s'offrent à vous, vous pouvez télécharger ce programme directement depuis le site officiel soit les liens suivants:

Télécharger les sources
Télécharger les paquets Debian (Ubuntu également)
Télécharger les paquest RPM

Le mieux reste sans doute d'utiliser l'outil de recherche/téléchargement/installation de votre distribution comme apt-get par exemple sous Debian et dérivés !
Voici en complément, un tutoriel d'initiation à DDD: Découverte du débogueur DDD

Valgrind :
Ceci est une suite d'outils de débogage, il permet entre autre de détecter les fuites mémoires dans vos programmes.
Comme pour DDD, vous pouvez soit télécharger le programme par le biais des programmes de votre distribution ou bien télécharger directement depuis le site officiel:

Télécharger Valgrind depuis le site officiel...
Image personnelle GDB :
Cet outil de débogage est en fait le même que celui sous Linux et dont DDD (cité plus haut sur Linux) en est une interface graphique (le programme GDB d'origine est en ligne de commande !). Vous pouvez télécharger ce programme depuis le site officiel:

Télécharger GDB depuis le site officiel...

XCode :
Si vous préférez les interfaces utilisateurs (ce qui reste encore le plus pratique), vous pouvez également avoir à votre disposition XCode qui est un EDI pour Mac OS X et qui intègre un "Frontend"(une interface graphique pour un programme en ligne de commande).

IV. Où puis-je trouver des exercices à faire ?

Il est toujours dur de trouver des exercices relativement corrects, les liens qui vous sont proposés ci-dessous vont aussi bien vers des exercices en rapport avec les maths qu'au développement de petits jeux en mode console et divers problèmes à résoudre !

Vous pouvez, pour commencer, trouver des défis sur notre page Défis C. Vous y trouverez les annales avec les diverses solutions proposées par les participants ainsi que le défis en cours s'il y a. Si cela ne vous suffit pas, voici quelques liens:

  • Récréomath
  • TopDev
  • Franc - IOI

Vous pouvez également entreprendre la création d'un petit jeu (toujours en mode console) comme par exemple :

  • Mastermind :
    • La première serait une version simple où le joueur devine la combinaison mise en place par l'ordinateur.
    • La seconde serait une version où c'est cette fois-ci, c'est l'ordinateur qui devine votre combinaison.
  • Puissance 4
  • Jeu de Dames

Ces exercices sont surtout intéressants du point de vue algorithmique et au niveau de l'IA (Intelligence Artificielle) !

V. Je veux aller plus loin :

Vous en avez assez des programmes en mode console ? Vous désirez faire des programmes avec une interface graphique (GUI: Graphic User Interface/Interface Utilisateur Graphique) ou voir même comment on peut créer des jeux vidéos en 2D et en 3D ? Regardez en-dessous !

V-A. Comment créer des applications graphiques ?

V-A-a. GTK+ (la GUI portable) :

La bibliothèque GTK+ permet de créer des programmes avec interfaces utilisateur tout en restant portable ce qui signifie que vous pouvez le faire tourner (sous quelques conditions tout de même) sur les systèmes comme Windows, Linux, et bien d'autres !

Voici des liens vous permettant d'apprendre à utiliser cette bibliothèque :

  • Cours GTK 2
  • GTK+ par l'exemple
  • La page GTK+ sur developpez.com
  • La FAQ GTK+ sur developpez.com

V-A-b. L'API Win32 (uniquement pour Windows) :

Contrairement à GTK+, l'API de Windows n'est pas portable et est sans doute un peu plus dure à prendre en main mais elle permet de faire des applications natives Win32 tout en ayant accès à toutes les fonctions de l'API Système !

Voici quelques liens pour vous familiariser avec cette API :

  • Tutorial d'initiation à la programmation avec l'API Windows
  • Cours sur l'API Windows en C
  • La page Windows sur developpez.com

V-B. Comment créer des jeux ?

V-B-a. La 2D :

La bibliothèque en vogue pour créer des jeux 2D (ou autre application multimédia) est la SDL (Simple Directmedia Layer). Cette bibliothèque est en effet surtout spécialisée pour la création de jeux vidéos en 2 Dimensions, elle est très simple d'emploi et on la maîtrise assez rapidement.

Voici quelques liens pour vous familiariser avec cette bibliothèque :

  • Tutoriaux SDL - Romain Perruchon
  • Introduction à la SDL
  • La page Jeux sur developpez.com
  • La FAQ SDL sur developpez.com

V-B-b. La 3D :

Connue de tous, la bibliothèque de référence dans la création de jeux vidéos 3D est sans conteste OpenGL !

Voici quelques liens pour vous permettre d'apprendre à l'utiliser :

  • Tutoriels NeHe en français - La traduction française des plus célèbres tutoriels OpenGL
  • La page Jeux sur developpez.com
  • La FAQ OpenGL sur developpez.com

VI. J'ai tout lu mais il y a des choses que je n'ai pas compris, que faire ?

Pas de panique ! Vous pouvez toujours poser vos questions sur les forums de developpez.com !
Voici les forums de developpez.com sur lesquels vous pouvez poser vos questions :

  • Forum : C
  • Forum : Code::Blocks
  • Forum : GTK+
  • Forum : SDL
  • Forum : OpenGL
  • Forum : Programmation Windows
  • Forum : Programmation Linux

Sans oublier les FAQ des rubriques :

  • FAQ : C
  • FAQ : GTK+
  • FAQ : SDL
  • FAQ : OpenGL

VII. Conclusion

J'espère que ce guide vous aura permis d'y voir un peu plus clair et de commencer votre apprentissage du Langage C plus sereinement.
L'équipe C/C++/Jeux vous souhaite une bonne programmation !


melem.developpez.com :: Initiation au langage C

Bonnes pratiques de codage en langage C

Table des matières

  • I. Présentation du code source
    • I-A. Jeu de caractères
    • I-B. Indentation
      • I-B-1. Comment bien présenter le code source
      • I-B-2. Tabulations contre espaces
    • I-C. Commentaires
      • I-C-1. Comment bien gérer les commentaires
      • I-C-2. Comment bien commenter
    • I-D. Éditeur de texte
      • I-D-1. Bien choisir son éditeur de texte
  • II. Conventions de nommage
    • II-A. Utilisation du caractère '_' (Underscore)
    • II-B. Macros
    • II-C. Constantes
      • II-C-1. Conventions typographiques
      • II-C-2. Identificateur
      • II-C-3. Définition
    • II-D. Types
      • II-D-1. Conventions typographiques
    • II-E. Objets (Variables)
      • II-E-1. Conventions typographiques
      • II-E-2. Identificateur
    • II-F. Fonctions
      • II-F-1. Conventions typographiques
      • II-F-2. Identificateur
  • III. Organisation du code source
    • III-A. Fichiers sources (*.c)
      • III-A-1. Liste ordonnée des éléments pouvant être contenus dans un fichier source
    • III-B. Fichiers d'en-tête (*.h)
      • III-B-1. Règles d'or régissant la définition des fichiers d'en-tête
      • III-B-2. Liste ordonnée des éléments pouvant être contenus dans un fichier d'en-tête
      • III-B-3. Comment bien utiliser les séparateurs
      • III-B-4. Protection contre les inclusions multiples
  • IV. Comment bien organiser son développement
  • V. Comment construire sa bibliothèque
    • V-A. Outil de développement gcc
    • V-B. Création d'une bibliothèque sous Code::Blocks 8.02
  • VI. Comment bien configurer son compilateur
    • VI-A. gcc
    • VI-B. Réglages dans Code::Blocks
    • VI-C. Réglages dans wxDev-C++ (remplace Dev-C++ devenu obsolète)
    • VI-D. Microsoft Visual C++ (6, 2005, 2008 etc.)
    • VI-E. Borland C / Turbo C
  • VII. Ressources

Cet article est une synthèse de plusieurs notes autour des bonnes pratiques de codage à adopter en langage C.

I. Présentation du code source

Il est extrêmement facile d'écrire du code illisible en C, que ce soit par une mauvaise présentation, ou par un usage abusif des formes contractées.

Il est, par contre très utile, de présenter du code clair. Cela aide à la compréhension, à la relecture, au contrôle visuel, à la maintenance.

I-A. Jeu de caractères

Le jeu de caractères normalisé par le langage C est

Sélectionnez
  A B C D E F G H I J K L M
  N O P Q R S T U V W X Y Z
  a b c d e f g h i j k l m
  n o p q r s t u v w x y z
  0 1 2 3 4 5 6 7 8 9
  ! " # % & ' ( ) * + , - . / :
  ; < = > ? [ \ ] ^ _ { | } ~

plus l'espace, et les caractères de commande représentants :

  • le saut de ligne ;
  • le retour chariot ;
  • la tabulation horizontale ;
  • la tabulation verticale ;
  • le saut de page.

L'utilisation de caractères supplémentaires (accentués, par exemple) invoque un comportement défini par l'implémentation.

Dans la pratique, la plupart des compilateurs acceptent les extensions courantes comme IBM PC8 et ISO 8859-1 (aussi appelées respectivement OEM et ANSI dans le monde MS-DOS/Windows).

Personnellement, j'évite d'utiliser les caractères autres que les caractères standards dans un code source, y compris dans les commentaires.

I-B. Indentation

I-B-1. Comment bien présenter le code source

Il existe de nombreuses façons de présenter le code C. Certaines sont plus prisées que d'autres, et je n'entrerai pas dans ce débat. Par contre, il est important, une fois qu'on a décidé de ce qui était "bien", de s'y tenir.

Tout le code présenté sur ce site observe des règles homogènes de présentation. Un moyen simple d'y parvenir est d'utiliser un outil unique pour la mise en page (indentation), tel que GNU Indent. Cet outil dispose de nombreux paramètres que personnellement j'ai fixés une fois pour toute, et qui confèrent à mes codes sources une certaine unité.

Sélectionnez
indent -bli0 -npsl -i3 -ts0 -sob

I-B-2. Tabulations contre espaces

Il est fortement recommandé d'utiliser un éditeur qui permet de remplacer les tabulations par des espaces (l'idéal étant que le nombre soit programmable). En effet, l'effet d'une tabulation n'est pas uniforme, alors que celui d'un espace l'est certainement d'avantage (à part en HTML, mais il y a des contournements possibles).

I-C. Commentaires

I-C-1. Comment bien gérer les commentaires

Le langage C, dans sa version normalisée ISO-1990, ne supporte que les commentaires de type /* comment */. L'imbrication des commentaires n'est pas autorisée.

Il est préférable d'éviter de placer des commentaires en bout de ligne. En effet, certains périphériques ou applications limitent la largeur visible du texte à moins de 80 colonnes. Ce serait dommage de se priver de commentaires qui peuvent être utiles. Il est donc recommandé de placer ses commentaires au-dessus de la portion de code à commenter.

Sélectionnez
/* compteur */
int cpt ;

Si un commentaire doit utiliser plusieurs lignes, il est recommandé d'utiliser la présentation 'ouverte', qui facilite la maintenance, tout en préservant la lisibilité. Exemple de forme minimale :

Sélectionnez
/* Ce compteur doit être initialise
   avant utilisation */
int cpt ;

S'il faut isoler provisoirement une portion de code, le mieux est de ne pas utiliser les commentaires (il pourrait y avoir des commentaires imbriqués), mais plutôt les directives du préprocesseur : #ifdef .. #endif ou #if .. #endif

Sélectionnez
#if 0
  /* Compteur */
  int cpt ;
#endif

I-C-2. Comment bien commenter

Le moins possible !

Le principe est de ne commenter que ce qui apporte un supplément d'information. Il est d'usage d'utiliser en priorité l'auto-documentation, c'est à dire un choix judicieux des identificateurs qui fait que le code se lit 'comme un livre'…

I-D. Éditeur de texte

I-D-1. Bien choisir son éditeur de texte

L'outil avec lequel un programmeur passe probablement le plus de temps est son éditeur de texte. Il entre dans une part importante de la productivité du programmeur et de la présentation du code.

Je n'entrerai évidemment pas dans le débat futile Vi/Emacs, car je mets tout le monde d'accord avec UltraEdit32 ! En fait peu importe, pourvu qu'il dispose au moins des fonctions suivantes :

  • police à chasse fixe ;
  • coloration syntaxique ;
  • la touche TAB produit des espaces (programmable, pour moi, c'est 3) ;
  • édition en mode colonne ;
  • annulation ;
  • copie de sauvegarde ;
  • patrons avec fonctions de base comme le nom du fichier et la date de création ;
  • possibilité d'ajouter des outils externes pour compléter ce qui manque.

II. Conventions de nommage

Une des façons d'obtenir du code clair est de s'en tenir à une convention de nommage des identificateurs qui soit cohérente et parlante. Voici mes recommandations.

II-A. Utilisation du caractère '_' (Underscore)

Ce caractère peut être utilisé comme séparateur visuel dans les identificateurs. Bien que ce soit techniquement possible, Je recommande de ne pas l'utiliser au début d'un identificateur, pour deux raisons principales :

  • La plupart de ces identificateurs sont réservés à l'implémentation du langage
  • L'aspect très inesthétique de ces identificateurs

II-B. Macros

Les macros ayant des usages multiples, il est difficile de faire une généralité. Dans les cas où elles représentent une constante, il est recommandé d'utiliser les majuscules. Dans les autres cas, le choix sera le même que pour l'identificateur qu'elles substituent. Cependant, si on cherche à insister sur le fait qu'une soi-disant fonction est en fait une macro, on peut utiliser les majuscules. En fait tout dépend du contexte.

II-C. Constantes

II-C-1. Conventions typographiques

Il est couramment admis que les constantes 'vraies' (macros, énumérations) doivent être écrites en majuscules. Les variables qualifiées const seront écrites comme des variables.

II-C-2. Identificateur

L'identificateur dépend du contexte. Il sert souvent à définir les valeurs des propriétés d'un objet (valeurs particulières, états, etc.). Il est recommandé d'utiliser un préfixe qui relie les constantes qui qualifient une même propriété.

Sélectionnez
  /*
  * Valeurs possibles de l'état du voyant
  *
  * OFF   : éteint
  * ON    : allume fixe
  * BLINK : clignotant
  */
  enum
  {
    VOYANT_STS_OFF,
    VOYANT_STS_ON,
    VOYANT_STS_BLINK,

    VOYANT_STS_NB
  };

II-C-3. Définition

Il y a deux façons de créer des valeurs constantes. Avec une macro ou avec une énumération.

Pour les constantes numériques de type int, il est recommandé d'utiliser les enum, qui présentent des avantages comme le typage, la numérotation automatique, ou l'interprétation textuelle automatique dans les débogueurs.

Pour les constantes numériques des autres types (long, unsigned etc.) et les chaînes, il n'y a pas le choix, il faut utiliser les macros. Ne pas oublier les qualificateurs de macros pour les valeurs numériques autres que int : Suffixes de macros numériques u ou U unsigned :

l ou L | long
ul ou UL | unsigned long
f ou F | float

Ne pas oublier non plus que pour qu'une constante soit de type double, au moins un des membres doit être de type double, donc comprendre un '.' dans son expression :

Sélectionnez
  #define PI (22/7)

est un entier qui vaut 3.

Sélectionnez
  #define PI (22/7.0)

est un double qui vaut environ 3.14.

II-D. Types

Le langage C permet de créer un alias sur un type existant plus ou moins complexe ou un autre alias existant à l'aide du mot clé typedef.

II-D-1. Conventions typographiques

Il est d'usage d'utiliser les minuscules. Il est recommandé d'utiliser des suffixes tels que :

Suffixes de types (alias)
_e | typedef enum
_u | typedef union
_s | typedef struct
_a | tableau (array)
_f | fonction

Il est recommandé de ne pas utiliser le suffixe '_t', car il est réservé par la norme POSIX pour définir des alias de types.

II-E. Objets (Variables)

II-E-1. Conventions typographiques

Il est d'usage d'utiliser les minuscules.

II-E-2. Identificateur

Il est un principe général de bonne conception qui veut que la longueur des identificateurs des objets et des fonctions soit proportionnelle à leur portée.

Les variables introduisent en plus la notion de durée de vie (duration). Il est bon qu'au premier coup d'œil, on sache exactement à quoi on a affaire. Il est bon de pouvoir aussi identifier rapidement les variables en fonction de leurs propriétés. Les plus remarquables sont :

  • simple (plain) ;
  • pointeur ;
  • tableau statique ;
  • tableau dynamique ;
  • chaîne de caractères.

Il est donc recommandé d'utiliser les préfixes suivants :

Préfixes d'identificateurs
 | simple
p_ | pointeur
a_ ou sa_ | tableau statique(static array)
da_ | tableau (dynamic array)
s_ | chaîne de caractères (string)

et en ce qui concerne la durée de vie et la portée :

Préfixes indiquant la durée de vie et la portée
 | Portée | Durée de vie
 | bloc | bloc
g_ | bloc | programme
S_ | module | programme
G_ | programme | programme

II-F. Fonctions

II-F-1. Conventions typographiques

Il est d'usage d'utiliser les minuscules. Il existe des cas où l'on a besoin de 2 mots pour nommer une fonction. Il n'est évidemment pas question d'accoler ces deux mots en minuscule, car le code serait vite illisible. Il existe 2 pratiques répandues :

  • coller les mots en mettant une majuscule au début de chaque mot :
Sélectionnez
OuvrirFichier()

ou (variante «à-la-Java») à partir du 2ème mot :

Sélectionnez
ouvrirFichier()
  • séparer les mots en mettant un souligné (underscore) entre chaque mot.
Sélectionnez
ouvrir_fichier()

C'est une question de goût, le principal est d'être clair et cohérent. La seconde méthode a ma préférence.

II-F-2. Identificateur

Pour une fonction, on choisit plutôt un identificateur qui exprime une action (verbe, verbe + substantif).

III. Organisation du code source

Il est pratique d'adopter une disposition logique et cohérente dans les fichiers sources et d'en-têtes. Il est d'usage d'utiliser un principe simple, qui consiste à définir ce que l'on doit utiliser. C'est pourquoi la disposition suivante est recommandée :

III-A. Fichiers sources (*.c)

III-A-1. Liste ordonnée des éléments pouvant être contenus dans un fichier source

  • inclusion des en-têtes (.h) nécessaires ;
  • définition des macros privées ;
  • définition des constantes privées ;
  • définition des types privés ;
  • définition des structures privées ;
  • définition des variables globales privées ;
  • définition des fonctions privées ;
  • définition des fonctions publiques ;
  • définition des variables globales publiques.

Voici par exemple, une liste de séparateurs utilisés pour structurer des fichiers sources :

Sélectionnez
  /* macros ============================================================== */
  /* constants =========================================================== */
  /* types =============================================================== */
  /* structures ========================================================== */
  /* private variables =================================================== */
  /* private functions =================================================== */
  /* internal public functions =========================================== */
  /* entry points ======================================================== */
  /* public variables ==================================================== */

III-B. Fichiers d'en-tête (*.h)

III-B-1. Règles d'or régissant la définition des fichiers d'en-tête

  • un fichier d'en-tête doit être protégé contre les inclusions multiples dans la même unité de compilation ;
  • un fichier d'en-tête doit être autonome[1] ;
  • un fichier d'en-tête ne doit inclure que le strict nécessaire à son autonomie.

[1] Pour s'en assurer, lors des tests de l'implémentation correspondante, le placer en 1ère position et ajouter ce qu'il manque dedans.

III-B-2. Liste ordonnée des éléments pouvant être contenus dans un fichier d'en-tête

  • inclusion des headers nécessaires et suffisants ;
  • définition des macros publiques ;
  • définition des constantes publiques ;
  • définition des types publiques ;
  • définition des structures publiques ;
  • déclaration des fonctions publiques ;
  • déclaration des variables globales publiques ;
  • [C99] définition des fonctions publiques inline.

Voici par exemple, une liste de séparateurs utilisés pour structurer des fichiers d'en-tête :

Sélectionnez
  /* macros ============================================================== */
  /* constants =========================================================== */
  /* types =============================================================== */
  /* structures ========================================================== */
  /* internal public functions =========================================== */
  /* entry points ======================================================== */
  /* public variables ==================================================== */
  /* inline public functions  ============================================ */

III-B-3. Comment bien utiliser les séparateurs

A l'origine, on a un fichier avec main() (par exemple main.c, comme ça, on voit tout de suite de quoi on parle). main() est le point d'entrée (unique) de ce module. On peut donc écrire :

Sélectionnez
/* main.c */
#include

/* entry points ======================================================== */

int main (void)
{
   puts ("hello world");

   return 0;
}

Le programme évoluant, on a maintenant une fonction privée (static) :

Sélectionnez
/* main.c */
#include

/* private functions =================================================== */

static void hello(void)
{
   puts ("hello world");
}

/* entry points ======================================================== */

int main (void)
{
   hello();

   return 0;
}

Nouvelle évolution, on crée un bloc fonctionnel (BF) hello composé pour le moment d'une seule fonction. On retrouve donc la structure habituelle avec 3 fichiers :

Sélectionnez
#ifndef H_HELLO
#define H_HELLO
/* hello.h */

/* entry points ======================================================== */

void hello(void);
Sélectionnez
/* hello.c */
#include "hello.h"
#include

/* entry points ======================================================== */

void hello(void)
{
   puts ("hello world");
}
Sélectionnez
/* main.c */
#include "hello.h"

/* entry points ======================================================== */

int main (void)
{
   hello();

   return 0;
}

On fait dans le luxe et on décompose la fonction hello() en plusieurs petites fonctions (un peu théorique, mais c'est pour montrer le principe).

Sélectionnez
/* hello.c */
#include "hello.h"
#include

/* private functions =================================================== */

static void h(void)
{
   printf ("hello");
}

static void w(void)
{
   printf ("world");
}

static void spc(void)
{
   putchar (' ');
}

static void eol(void)
{
   putchar ('\n');
}

/* entry points ======================================================== */

void hello(void)
{
   h();
   spc();
   w();
   eol();
}

Le reste (interface, allocation) est inchangé.

Attention, étape ultime et subtile.

Le fichier d'implémentation de hello est devenu tellement gros qu'il nécessite lui-même un découpage. Il va donc falloir distinguer ce qui est accessible à l'utilisateur de hello (les points d'entrées) et les fonctions 'internes', c'est à dire connues de hello mais pas de l'application, d'où cette distinction, qui se traduit aussi par des headers distincts :

Sélectionnez
/* hellotools.c */
#include "hellotools.h"
#include

/* internal public functions =========================================== */
void hello_spc(void)
{
   putchar (' ');
}

void hello_eol(void)
{
   putchar ('\n');
}
Sélectionnez
#ifndef H_HELLOTOOLS
#define H_HELLOTOOLS
/* hellotools.h */

/* internal public functions =========================================== */
void hello_spc(void);
void hello_eol(void);

#endif
Sélectionnez
/* hello.c */
#include "hellotools.h"
#include "hello.h"

#include

/* private functions =================================================== */
static void h(void)
{
   printf ("hello");
}

static void w(void)
{
   printf ("world");
}

/* entry points ======================================================== */
void hello(void)
{
   h();
   hello_spc();
   w();
   hello_eol();
}

Cela revient à créer plusieurs niveaux de visibilité…

III-B-4. Protection contre les inclusions multiples

Les fichiers d'en-tête pouvant être inclus dans des fichiers sources, comme dans des fichiers d'en-tête, et ce, dans un ordre non spécifié, il est indispensable de se prémunir contre les risques d'inclusions multiples dans la même unité de compilation.

Sélectionnez
  #ifndef IDENTIFICATEUR
  #define IDENTIFICATEUR

  /* zone protégée contre les inclusions multiples */

  #endif /* guard */

Le principe de protection étant basé sur la définition d'une macro, cette macro doit être unique afin d'éviter les protections abusives.

Personnellement, j'utilise le format suivant :

Sélectionnez
  H_<initials>_<file>_<date>
  initials ::= En majuscule : initiales du développeur, nom de la société etc.
  file     ::= En majuscule : nom du fichier (sans l'extension)
  date     ::= <year><month><day><hour><minute><second>
  year    ::= année (0000-9999)
  month   ::= mois (01-12)
  day     ::= jour (01-31)
  hour    ::= heure (00-23)
  minute  ::= minute (00-59)
  second  ::= seconde (00-59)

Par exemple :

Sélectionnez
  H_ED_EXEMPLE_20040529115235

Le choix de mettre le H_ en tête est justifié. En effet, il évite de briser une règle du langage C qui dit qu'un identificateur commençant par E suivi d'une majuscule est réservé pour l'implémentation. En fait ils sont utilisés pour implémenter les valeurs symboliques de errno.

IV. Comment bien organiser son développement

Le langage C autorise la compilation séparée. Cette technique permet de créer des unités de compilations (compile units) séparées. Une bonne maîtrise de la programmation permet de réaliser du code indépendant ou tout au moins suffisamment indépendant pour être testable individuellement.

Voici un exemple élémentaire de compilation séparée reprenant le célèbre "Hello world!". Le code est divisé en 3 sources. Un fichier d'interface (hello.h), un fichier d'implémentation (hello.c) et un fichier d'application (main.c) :

main.c
Sélectionnez
  /* main.c */

  #include "hello.h"

  int main (void)
  {
    hello ();
    return 0;
  }
hello.h
Sélectionnez
  #ifndef H_HELLO
  #define H_HELLO

  /* hello.h */

  void hello (void);

  #endif /* guard */
hello.c
Sélectionnez
  /* hello.c */

  #include "hello.h"
  #include <stdio.h>

  void hello (void)
  {
    puts ("Hello world!");
  }

Vidéo sonore (flash) expliquant comment faire sous Code::Blocks 8.02

Afin de bien organiser les fichiers, je conseille de créer des répertoires pour les sources et pour les fichiers de sortie :

Organisation des répertoires
Type | Répertoire | Fichiers | nom.ext
Génération | ./ | make | Makefile
Source | inc/ | inclus | *.h
Source | src/ | source | *.c
Sortie | obj/ | objets intermédiaires | *.obj
Sortie | exe/ | exécutable | *.exe

et par conséquent, d'adopter l'organisation suivante pour les répertoires :

Sélectionnez
  hello/Makefile
  hello/inc/hello.h
  hello/src/hello.c
  hello/src/main.c
  hello/obj/hello.obj
  hello/obj/main.obj
  hello/exe/hello.exe

Afin de garantir un comportement correct du code et des outils de développement, je recommande que les noms de répertoires et de fichiers soient tous écrits en minuscule. Je rappelle que si on utilise un outil de gestion de version comme CVS, il est extrêmement difficile de modifier la casse des répertoires et même des fichiers après coup. C'est donc une démarche à adopter dès la première ligne de code.

Pour compiler et exécuter ce code sous gcc avec cette organisation, on peut utiliser ce Makefile (DJGPP). (Remplacer <tab> par une véritable tabulation) :

Sélectionnez
  # Makefile HELLO

  # Paths
  DSRC = src
  DINC = inc
  DOBJ = obj
  DEXE = exe
  DLIB = d:/djgpp/lib

  # Compiler flags
  CFLAGS = -I$(DINC)

  # Commands
  CC = gcc -c $(CFLAGS)
  LK = ld
  RM = del

  # Project list
  OBJS =      \
  $(DOBJ)/hello.obj  \
  $(DOBJ)/main.obj   \

  #Rebuild all target
  all: $(DEXE)/hello.exe

  # Clean all target
  clean:
  <tab>cd $(DOBJ)
  <tab>$(RM) *.obj
  <tab>cd ../$(DEXE)
  <tab>$(RM) *.exe
  <tab>cd ..

  # main target (OBJS + init code + library)
  $(DEXE)/hello.exe : $(OBJS)
  <tab>$(LK) -o $(DEXE)/hello.exe $(OBJS) $(DLIB)/crt0.o -lc

  # objects

  # The hello.c file
  $(DOBJ)/hello.obj : $(DSRC)/hello.c          \
            $(DINC)/hello.h           \
            Makefile
  <tab>$(CC) $(DSRC)/hello.c -o$(DOBJ)/hello.obj

  # The main.c file
  $(DOBJ)/main.obj : $(DSRC)/main.c            \
            $(DINC)/hello.h           \
            Makefile
  <tab>$(CC) $(DSRC)/main.c -o$(DOBJ)/main.obj

Avec une utilisation comme suit (MS-DOS) :

  • génération ou mise à jour :
Sélectionnez
  D:\HELLO>make
  gcc -c -Iinc src/hello.c -oobj/hello.obj
  gcc -c -Iinc src/main.c -oobj/main.obj
  ld -o exe/hello.exe obj/hello.obj obj/main.obj  d:/djgpp/lib/crt0.o -lc
  • effacement complet :
Sélectionnez
  D:\HELLO>make clean
  cd obj
  del *.obj
  cd ../exe
  del *.exe
  cd ..

Pour en savoir plus, consultez l'article l'outil MAKE

V. Comment construire sa bibliothèque

Le langage C utilisant lui même la notion de bibliothèque de fonctions, il est logique que cette possibilité soit offerte aux développeurs. Les avantages sont bien connus :

  • identification et bonne gestion du code réutilisable ;
  • interface bien défini (headers) ;
  • gain de productivité (réutilisation,moins de code à compiler).

Les détails de réalisation dépendent des outils de développement, mais le principe d'organisation du code est le même. La compilation d'une bibliothèque est évidemment une application directe du principe de la compilation séparée.

S'ajoutent quelques règles de bon sens telles que l'indépendance du code vis à vis de l'application. Cela concerne particulièrement les sorties qui, si il le faut, seront implémentées par des 'callbacks' (détails dans cet article : Concevoir et réaliser un composant logiciel en C ). Plus que jamais, les globales seront évitées.

Pour réaliser la bibliothèque, il faut créer un projet rassemblant les fichiers d'interface (headers : .h) et les fichiers d'implémentation (sources : .c). Évidemment, il ne doit pas y avoir de fonction main().

Ensuite, après compilation classique, et sans faire d'édition de lien, bien évidemment, on utilise un 'librarian' qui est un outil particulier faisant partie des outils de développements courants, et qui sert à créer la bibliothèque. Le résultat est un fichier dont l'extension dépend de l'outil. (par exemple, .lib ou .a).

V-A. Outil de développement gcc

Tout d'abord, avec gcc, il y a des règles de nommage à respecter. En effet, le nom du fichier produit doit impérativement commencer par lib et son extension doit être .a (comme archive).

Ensuite, l'outil s'appelle ar (ar.exe sous DOS/Windows) comme archiver. Les détails d'appels en mode commande sont à lire dans la documentation de gcc. Comme toujours en mode ligne de commande, un Makefile simplifie et automatise la tâche de production du code.

Les exemples suivants utilisent le fichier d'interface hello.h et le fichier d'implémentation hello.c décrits dans l'article sur la compilation séparée. La bibliothèque à créer s'appelle libhello.a. Elle ne sert évidemment qu'à illustrer le propos.

V-B. Création d'une bibliothèque sous Code::Blocks 8.02

  • File > New > Project ;
  • sélectionner 'static library' ;
  • sélectionner un répertoire <racine> et taper le nom de la bibliothèque : libhello /!\ IMPORTANT /!\ ;
  • le répertoire <racine>/libhello est créé automatiquement ; c'est le répertoire de travail ;
  • Debug et Release sont cochées, ne rien changer ;
  • cliquer sur [finish].

Dans le 'workspace', une ligne en gras a été ajoutée : 'libhello'. Un fichier main.c a été créé. On peut le retirer du projet :

  • Clic droit > Remove from project

Il faut maintenant ajouter les 2 fichiers source (.c et .h). Si il n'y sont pas déjà, les déplacer (ou les créer) dans le répertoire du projet (<racine>/libhello), puis :

  • sur la ligne 'Biblio. Hello' : click droit ;
  • Add files ;
  • vérifier qu'on est bien dans le répertoire <racine>/libhello ;
  • (on en profite pour supprimer main.c) ;
  • sélectionner hello.h et hello.c ;
  • valider ;
  • compiler.

La fenêtre de compilation doit donner quelque chose comme ceci :

Sélectionnez
-------------- Build: Debug in libhello ---------------

Compiling: hello.c
Linking static library: libhello.a
ar.exe: creating libhello.a
Output size is 3.63 KB
Process terminated with status 0 (0 minutes, 0 seconds)
0 errors, 0 warnings

On voit que hello.c a été compilé, que ar.exe a été invoqué et que libhello.a a été crée. On peut maintenant créer un petit programme de test avec le main.c et la bibliothèque. Le plus simple pour le moment est de la créer dans le même répertoire.

Sélectionnez
  Project   : Test hello
  Compiler  : GNU GCC Compiler (called directly)
  Directory : C:\dev\hello\
  --------------------------------------------------------------------------------
  Switching to target: default
  Compiling: main.c
  Linking console executable: C:\dev\hello\test.exe
  .objs\main.o: In function `main':
  C:/dev/hello/main.c:7: undefined reference to `hello'
  collect2: ld returned 1 exit status
  Process terminated with status 1 (0 minutes, 2 seconds)
  1 errors, 0 warnings

Bien sûr, le main.c tout seul ne suffit pas, il faut ajouter la bibliothèque au projet :

  • sur la ligne 'Test biblio. Hello' : clic droit ;
  • build options > Linker > Add > [...] > libhello.a > Ouvrir > NON > OK > OK ;
  • compiler.

On obtient alors :

Sélectionnez
  Project   : Test hello
  Compiler  : GNU GCC Compiler (called directly)
  Directory : C:\dev\hello\
  --------------------------------------------------------------------------------
  Switching to target: default
  Linking console executable: C:\dev\hello\test.exe
  Process terminated with status 0 (0 minutes, 0 seconds)
  0 errors, 0 warnings

et le fameux

Sélectionnez
  Hello world!

  Press ENTER to continue.

tant espéré !

VI. Comment bien configurer son compilateur

Le compilateur dispose de moyens d'analyse du code qui peuvent être mis à profit pour détecter toutes sortes d'erreurs. Il est donc important de savoir configurer son compilateur pour qu'il signale au mieux les anomalies pouvant se produire dans le code (Warnings). C'est ensuite au programmeur de réagir et, soit de justifier ou d'expliquer l'avertissement, soit de corriger le code.

La première des vérifications à faire est de s'assurer que l'on compile avec le bon compilateur. Certaines extensions ou réglages par défaut font que c'est parfois le compilateur C++ qui est invoqué au lieu du compilateur C. Ces quelques lignes placées au début de chaque fichier source (.c) permettent de détecter cette erreur :

Sélectionnez
#ifdef __cplusplus
#error This source file is not C++ but rather C. Please use a C-compiler
#endif

Si l'erreur se produit, vérifier les réglages et l'extension. Celle-ci doit impérativement être .c (minuscule), et non .cpp, ni .C (majuscule)

VI-A. gcc

gcc est le 'gnu c compiler', version GNU du cc Unix pour GNU/Linux. Il a été porté sur de nombreuses plate-formes dont Windows (les plus connues sont MinGW et CygWin).

Par défaut, le niveau d'avertissement (Warnings) est très laxiste. Il est fortement recommandé d'utiliser la configuration minimale suivante (C90) :

Sélectionnez
-Wall -Wextra -ansi -O -Wwrite-strings -Wstrict-prototypes -Wuninitialized
-Wunreachable-code

Sur les anciennes version de gcc (< 3.4.x), remplacer -Wextra par -W.

Si la version de gcc le permet, il est possible d'ajouter ces options :

Sélectionnez
-Wno-missing-braces  -Wno-missing-field-initializers

Experts uniquement

Il est aussi possible de procéder à un réglage 'fin'. Voici comment je configure gcc en mode 'paranoïaque' (C90) :

Sélectionnez
-O2 -Wchar-subscripts -Wcomment -Wformat=2 -Wimplicit-int
-Werror-implicit-function-declaration -Wmain -Wparentheses
-Wsequence-point -Wreturn-type -Wswitch -Wtrigraphs -Wunused
-Wuninitialized -Wunknown-pragmas -Wfloat-equal -Wundef
-Wshadow -Wpointer-arith -Wbad-function-cast -Wwrite-strings
-Wconversion -Wsign-compare -Waggregate-return -Wstrict-prototypes
-Wmissing-prototypes -Wmissing-declarations -Wmissing-noreturn
-Wformat -Wmissing-format-attribute -Wno-deprecated-declarations
-Wpacked -Wredundant-decls -Wnested-externs -Winline -Wlong-long
-Wunreachable-code

Attention ce réglage révèle certains défauts dans les headers de MinGW. La correction est possible, mais uniquement si on sait ce qu'on fait. Je n'en dirai donc pas plus ici.

Le détail de chaque paramètre est expliqué dans la documentation de gcc.

VI-B. Réglages dans Code::Blocks

  • Settings > Compiler > Onglet 'Compiler' > Onglet 'Other options' ;
  • copier/coller les options ;
  • OK ;
  • régénérer (Ctrl-F11).

S'assurer qu'une coche ne traine pas dans l'onglet de configuration du compilateur.

Plus d'informations sur Code::Blocks.

VI-C. Réglages dans wxDev-C++ (remplace Dev-C++ devenu obsolète)

  • outils > Options du compilateur ;
  • dans la zone de saisie "Ajouter les commandes suivantes lors de l'appel du compilateur", copier/coller les options que je recommande ;
  • valider ;
  • régénérer (Ctrl-F11).

VI-D. Microsoft Visual C++ (6, 2005, 2008 etc.)

  • propriétés du projet -> C/C++ -> général ;
  • mettre le niveau (level) de warning à 4.

Plus d'informations sur Microsoft Visual C++ Express 2008 et 2010

VI-E. Borland C / Turbo C

  • Option -> Compiler -> Messages -> Display ;
  • (*) ALL.

VII. Ressources

  • Recommended C Style and Coding Standards ;
  • MISRA ;
  • Macros prédéfinies.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit


melem.developpez.com :: Initiation au langage C

Initiation pragmatique au langage C

Table des matières

  • I. Introduction
  • II. Programme minimum
    • II-A. Analyse détaillée
  • III. Un programme qui dit "bonjour"
  • IV. Glossaire
    • IV-A. Bit
    • IV-B. Byte
    • IV-C. Caractère
    • IV-D. Chaîne de caractères
    • IV-E. Comportement indéfini
    • IV-F. Fonction
    • IV-G. Flux
    • IV-H. Paramètre
    • IV-I. Préprocesseur
    • IV-J. printf()
      • IV-J-1. Les principaux formateurs de printf()
      • IV-J-2. Exemples d'utilisation de printf()
    • IV-K. Prototype
    • IV-L. sizeof
    • IV-M. Tableau
    • IV-N. Type

Présentation des bases du langage C par une approche progressive, mais concrète et basée sur l'expérimentation.

Ces bases sont suivies d'un glossaire de termes courants en programmation C.

I. Introduction

Il existe de nombreuses façons d'expliquer les bases d'un langage de programmation. J'ai choisi une approche progressive, mais concrète et basée sur l'expérimentation. Les informations données ne prétendent pas être exhaustives, et font référence à la définition du langage C selon la norme ISO/IEC 9899:1990 (appelée aussi C90).

Pour les détails, il est conseillé de se munir d'un livre de référence comme The C programming language (et son correctif) de Brian Kernighan et Dennis Ritchie ou sa version française : Le langage C. Les solutions aux exercices sont dévoilées ici.

Le langage C est un langage normalisé qui comprend des instructions de préprocesseur, des instructions C, et des symboles dont la sémantique peut varier selon le contexte.

II. Programme minimum

Voici le programme C le plus simple que l'on puisse écrire.

int main (void)
{
   return 0;
}

Ce programme est composé de plusieurs mots clés faisant partie du langage C : int, void et return

Il utilise aussi 5 symboles syntaxiques, à savoir (, ), {, } et ;. Enfin, il utilise un mot défini par l'utilisateur : main.

Ce programme ne fait rien de visible. Cependant, il met en œuvre la structure de code de base du langage C, à savoir la fonction.

II-A. Analyse détaillée

Nous allons étudier les mots et le symboles qui constituent le programme dans l'ordre de leur apparition.

int est un type. Ici, il qualifie la valeur retournée par la fonction.

main est un identificateur défini par l'utilisateur. Utilisé dans ce contexte, il désigne le nom de la fonction. Cet identificateur a cependant un sens particulier. Il désigne le point d'entrée du programme. Cela signifie qu'un programme C commencera toujours par un appel 'invisible' à la fonction main(). Cet appel provient de la séquence d'initialisation du programme. Les détails dépendent de l'implémentation, mais, d'une manière simplifiée, voici quel est le déroulement des opérations :

  1. chargement du logiciel ;
  2. exécution du code d'initialisation ("boot") ;
  3. appel de l'application (CALL MAIN) ;
  4. exécution de l'application, jusqu à la fin de main() ;
  5. exécution du code de fin ;
  6. retour au système.

void est un mot clé qui signifie 'rien' ou 'absence de'. Ici, il sert à indiquer que la fonction n'a pas de paramètres.

return est un mot clé qui signifie "quitter la fonction courante". Si la fonction n'a pas de type de retour (type void) on peut utiliser return tout seul suivit d'un point-virgule. Sinon, il doit être suivi d'une valeur et d'un point-virgule. La valeur doit être du type qui a été défini pour la fonction. Cette valeur peut avoir un sens particulier. Il est d'usage de retourner 0 pour signifier que tout va bien (pas d'erreur) et une valeur autre que 0 pour signifier une erreur.

L'usage d'un 'return' sans valeur de retour dans une fonction qui attend une valeur de retour provoque un comportement indéfini.

III. Un programme qui dit "bonjour"

Voici une version un peu plus élaborée, qui se contente d'afficher une simple phrase ("Hello world!") sur la sortie standard (généralement l'écran).

#include <stdio.h>

int main (void)
{
   puts ("Hello world!");
   return 0;
}

Dans ce programme, on constate l'ajout du mot clé #include et de son paramètre <stdio.h>, ainsi que la fonction puts(). et de son paramètre "Hello world!".

On remarque que le mot clé #include commence par un '#'. En effet, ce mot clé appartient à la famille des commandes du préprocesseur, dont une caractéristique est justement de commencer par '#'.

#include <stdio.h> signifie que le fichier indiqué en paramètre est inclus dans le code source. Cette opération (ainsi que toutes les opérations du préprocesseur) est effectuée avant la traduction du code.

Cette opération d'inclusion est rendue nécessaire à cause de l'appel à la fonction puts(). En effet, il est recommandé (et parfois obligatoire) de fournir au compilateur un prototype à la fonction utilisée :

Sélectionnez
int puts (char const *);

La fonction puts() appartient à la bibliothèque standard du langage C, qui fait partie intégrante du langage. La définition du prototype de cette fonction se trouvant dans le fichier d'en-tête <stdio.h>, il est donc tout à fait logique d'inclure ce fichier dans le fichier source.

Cette fonction a pour effet d'émettre la chaîne de caractères passée en paramètre vers le flux de sortie standard. Elle ajoute automatiquement un caractère de fin de ligne.

A première vue, le paramètre de cette fonction est une chaîne de caractères, En fait, c'est l'adresse de celle-ci. C'est pourquoi le paramètre de fputs() est défini comme un pointeur sur un type char. En effet, un pointeur est une variable qui peut contenir une adresse.

Le qualificateur const signifie que la fonction accepte l'adresse d'une chaîne non modifiable, car elle s'engage à ne pas modifier la chaîne pointée, ni à passer son adresse à une fonction qui pourrait la modifier.

IV. Glossaire

IV-A. Bit

Le bit (BInary digiT) ou chiffre binaire est l'unité de mesure d'information. Il peut prendre 2 valeurs : 0 ou 1.

IV-B. Byte

Le byte (ou multiplet) est le plus petit objet adressable pour une implémentation donnée. En langage C, il fait au moins huit bits.

IV-C. Caractère

Un caractère est une valeur numérique qui représente un glyphe visible ('A', '5', '*' etc.) ou non (CR, LF etc.). Un caractère est de type int. Cependant, sa valeur tient obligatoirement dans un type char.

Un objet de la taille d'un byte peut recevoir la valeur de n'importe quel caractère.

IV-D. Chaîne de caractères

Une chaîne de caractères est une séquence de caractères encadrée de double quotes.

"Hello world!"

La représentation interne d'une chaîne de caractères est spécifiée. C'est un tableau de char terminé par un 0.

char s[] = "Hello";

est équivalent à :

char s[] = {'H', 'e', 'l', 'l', 'o', 0};

IV-E. Comportement indéfini

Un comportement indéfini (encore appelé Undefined Behaviour ou UB) est un bug grave. Il signifie que le comportement du programme est imprévisible et que tout peut arriver, y compris, et c'est ça qui rend ce bug très dangereux, un comportement d'apparence conforme.

Il n'existe pas de moyen automatique de détecter tous les UB d'un programme. Seul un contrôle visuel fait par une personne chevronnée permet de détecter un tel bug.

IV-F. Fonction

Une fonction est une séquence d'instructions dans un bloc nommé. Une fonction est constituée de la séquence suivante :

  • un type (ou le mot clé void) ;
  • un identificateur (le nom de la fonction, ici main) ;
  • une parenthèse ouvrante ( ;
  • une liste de paramètres ou une liste vide (dans ce cas, on écrit void) ;
  • une parenthèse fermante ) ;
  • une accolade ouvrante { ;
  • une liste d'instructions terminée par un point virgule ;, ou rien ;
  • une accolade fermante }.

L'utilisateur peut créer ses propres fonctions. Une fonction peut appeler une autre fonction. Généralement, une fonction réalisera une opération bien précise. L'organisation hiérarchique des fonctions permet un raffinement en partant du niveau le plus élevé (main()) en en allant au niveau le plus élémentaire (atomique).

On gardera cependant en tête que la multiplication des niveaux introduit une augmentation de la taille du code et du temps de traitement.

Une fonction ne peut être appelée qu'à partir d'une autre fonction.

IV-G. Flux

Un flux est un canal de données orienté byte. Il permet de réaliser des entrées ou des sorties de bytes, soit un par un, soit par bloc.

C'est le mécanisme du langage C qui permet l'interaction avec l'environnement.

Un programme C ouvre 3 flux par défaut :

  • stdin : le flux d'entrée standard ;
  • stdout : le flux de sortie standard ;
  • stderr : le flux de sortie d'erreur.

Sur la plupart des implémentations, ces flux sont connectés à la console.

IV-H. Paramètre

La zone entre parenthèses d'une fonction peut recevoir des paramètres. Un paramètre est défini au minimum par un type et un identificateur.

Exemple :

int fonction (int x)
{
}

x est un paramètre de type int. Il est obligatoire, et la valeur passée doit être du même type.

...
{
   fonction (123);
}

...
{
   int a = 123;
   fonction (a);
}

Il faut savoir qu'en C, un paramètre ne fait que transmettre une valeur. Modifier la valeur d'un paramètre n'aura jamais d'effet sur la valeur originale. (la fonction printf() permet ici de visualiser la contenu des variables a et x) :

Sélectionnez
#include <stdio.h>

void fonction (int x)
{
   printf ("x = %d (dans la fonction)\n", x); /* x = 123 */

   x = 456;
   printf ("x = %d (dans la fonction)\n", x); /* x = 456 */
}

int main (void)
{
   int a = 123;

   printf ("a = %d\n", a); /* a = 123 */

   fonction (a);
   printf ("a = %d\n", a); /* a = 123 */
   return 0;
}

Après l'appel de la fonction, la valeur de 'a' est inchangée (123).

Sélectionnez
a = 123
x = 123 (dans la fonction)
x = 456 (dans la fonction)
a = 123

Le langage C permet la modification, mais en utilisant des techniques plus avancées.

IV-I. Préprocesseur

Le préprocesseur est un outil qui traduit certaines instructions du code source avant le compilateur. Les instructions du préprocesseur commencent par #. Par exemple #include.

IV-J. printf()

printf() est une fonction standard de la bibliothèque du la bibliothèque du C. Elle est conçue pour émettre des chaînes de caractères formatées vers la sortie standard (stdout). Elle est déclarée dans le fichier d'entête <stdio.h>. Pour l'utiliser, on doit utiliser la directive #include <stdio.h>.

Cette puissante fonction permet de réaliser des affichages formatés. Sa description complète prendrait un chapitre entier. Voici donc quelques éléments de base.

Elle fonctionne selon un principe un peu particulier :

Son premier paramètre est une chaîne de caractères pouvant comporter des séquences spéciales commençant par "%". Le caractère qui suit le "%" a une signification particulière. Certaines sont définies (%, d, i, f, s, c etc.), d'autres non. Le rôle est cette séquence de caractères est d'indiquer à printf() comment il doit interpréter les valeurs passées dans les paramètres suivants. Il doit y avoir correspondance, sinon, le comportement est indéfini.

IV-J-1. Les principaux formateurs de printf()

Formateur de base | Rôle | Type attendu | Types compatibles
% | affiche le glyphe du caractère % | int | short, char
c | affiche le glyphe d'un caractère imprimable | int | short, char
d | affiche la valeur d'un entier en décimal | int | short, char
f | affiche la valeur d'un réel en décimal avec virgule fixe | double | float
s | affiche les glyphes d'une chaîne de caractères | char * |

IV-J-2. Exemples d'utilisation de printf()

#include <stdio.h>

int main (void)
{
   printf ("%c\n", 'A');
   printf ("%d\n", 123);
   printf ("%d, '%c'\n", 'A', 'A');

   printf ("Hello world\n");
   printf ("Hello %s\n", "world");
   printf ("%s\n", Hello world);

   return 0;
}

produit :

A
123
65, 'A'
Hello world
Hello world
Hello world

Les possibilités complètes de cette puissante fonction sont décrites dans un livre de C comme ceux recommandés au début de ce chapitre.

IV-K. Prototype

Le prototype d'une fonction est une déclaration indiquant :

  • le type du retour (ou void si il n'y en a pas) ;
  • le nom de la fonction ;
  • la liste et le type des paramètres (ou void si il n'y en a pas).

Un prototype peut être intégré à la fonction. Dans ce cas, il est confondu avec la première ligne de la fonction :

Sélectionnez
int function (void)
{
   /* ... */
   return 0;
}

Il peut aussi être détaché (séparé) de la fonction. Dans ce cas, il doit être suivi d'un ';'.

Sélectionnez
int function (void);

IV-L. sizeof

L'opérateur unaire sizeof retourne la taille d'un objet en bytes. Le paramètre de sizeof peut être un objet ou un type. Si c'est un type, celui-ci doit être placé entre parenthèses. Le type retourné par sizeof est size_t (entier non signé). L'expression est une constante évaluée à la compilation.

size_t est un type standard défini dans <stddef.h>.

Sélectionnez
#include <stddef.h>

size_t size_of_an_int = sizeof (int);

double n;

size_t size_of_n = sizeof n;

IV-M. Tableau

Un tableau est une suite d'objets identiques consécutifs.

Sélectionnez
/* Définition d'un tableau de 10 char */
char tab[10];

Il est possible d'initialiser un tableau au moment de sa définition. Les membres non initialisés seront forcés à 0 :

Sélectionnez
/* Définition d'un tableau de 20 entiers
 * de type long avec initialisation a 0
 */
long tab[20] = {0};

/* Définition d'un tableau de 5 flottants
 * de type double avec initialisation partielle
 */
double tab[5] = {12.34, 56.78};

Enfin, il est possible de déterminer la taille d'un tableau par son initialisation. La taille s'ajuste alors en fonction du nombre d'initialiseurs :

Sélectionnez
/* Définition d'un tableau de 6 entiers
 * de type int initialises
 */
int tab[] = {1, 2, 3, 4, 5, 6};

Grâce à l'opérateur sizeof, il est possible de déterminer le nombre d'éléments d'un tableau. Il suffit de diviser la taille du tableau (en bytes) par la taille d'un élément du tableau (en bytes) (par exemple, [0], mais la valeur exacte de l'indice est sans importance).

Sélectionnez
/* Nombre d'éléments de 'tab' */
size_t nb_elem = sizeof tab / sizeof tab[0];

IV-N. Type

Le type est une des propriétés qui caractérise une valeur. Elle permet de préciser quelle est la gamme de valeurs possible. La norme qui définit le langage C précise qu'en fonction du type, ces valeurs doivent couvrir une gamme minimale.

Il y a quatre mots-clés du langage C qui définissent des types :

  • char : petit entier, convient pour les caractères ;
  • int : pour les valeurs numériques entières ;
  • float : nombres à virgule flottante en simple précision ;
  • double : nombres à virgule flottante en double précision.

On peut ajouter des mots-clés particuliers pour modifier la plage de valeurs (short, long) et / ou le fait que la valeur soit signée ou non (signed, unsigned), ce qui décale la plage de valeurs. L'ordre n'a pas d'importance, du moment que ces mots sont placés avant l'identificateur de type.

Voici les types entiers courants avec leurs gammes de valeurs minimales. Les termes entre [] sont facultatifs.

Type | Minimum | Maximum
char | 0 | 127
unsigned char | 0 | 255
signed char | -127 | 127
[signed] short [int] | -32767 | 32767
unsigned short [int] | 0 | 65535
[signed] int | -32767 | 32767
unsigned [int] | 0 | 65535
[signed] long [int] | -2147483647 | 2147483647
unsigned long [int] | 0 | 4294967295

Et uniquement en C99 :

Type | Minimum | Maximum
[signed] long long [int] | -9223372036854775807 | 9223372036854775807
unsigned long long [int] | 0 | 18446744073709551615

On peut mentionner aussi un type défini par l'implémentation, qui est size_t. C'est le type qui est retourné par l'opérateur sizeof. Ce type (entier non signé) est conçu pour contenir des informations du genre "taille d'un objet". Par conséquent, il convient aussi aux indices (croissants) des tableaux ainsi qu'aux nombres d'éléments des tableaux.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit


Langage C/Introduction au C — Wikiversité

Langage C/Introduction au C

Une page de Wikiversité. http://fr.wikiversity.org/wiki/Langage_C tutoriel C à voir sur wikiversity
Introduction au C
Icône de la faculté
Chapitre no1
Leçon : Langage C
Retour au | sommaire
Chap. suiv. : | Outils
Icône de titre

Sommaire

  • 1 Avant-propos
    • 1.1 Esprit et but du tutoriel
    • 1.2 À qui est destiné ce tutoriel ?
    • 1.3 Allez plus loin
  • 2 La programmation, qu’est-ce que c’est ?
  • 3 Le langage C
    • 3.1 L'histoire du C
    • 3.2 Pourquoi apprendre le C ?
    • 3.3 La norme
  • 4 L’algorithmique
    • 4.1 Le pseudo-code
  • 5 Voir aussi

Avant-propos

Esprit et but du tutoriel

Ce tutoriel a été écrit dans un seul but : vous enseigner le langage C de la manière la plus complète, la plus rigoureuse et la plus instructive possible. Pour ce faire, le tutoriel combinera beaucoup de théorie, de connaissances importantes, de détails et de recherches en profondeur avec de la pratique par des exemples concrets, des exercices pratiques et des TP. Cette approche va réclamer de votre part des efforts puisque le tutoriel semblera parfois complexe, néanmoins il est important de passer tout le temps nécessaire à l'assimilation de chaque connaissance abordée. Pour s'assurer d'avoir bien assimilé une notion, il est fortement conseillé de se façonner ses propres programmes tout au long du cours, et ce en plus des exercices proposés au sein de celui-ci, c'est en vous exerçant que vous prendrez conscience de vos faiblesses, et que vous pourrez progresser.

Nous avons choisi cette méthode d’apprentissage, car c’est celle que nous jugeons la plus payante. Elle s’oppose à d'autres méthodes qui privilégient un cours beaucoup plus théorique, qui permettent certes d’acquérir des connaissances rapidement, mais qui s’avère bien souvent peu payante sur le long terme. Beaucoup de programmeurs débutants se sont retrouvés perdus lorsqu'ils se sont lancés dans la création d'un projet pour la première fois à la sortie d’un cours purement théorique, n’ayant pas acquis de bonnes pratiques et de la rigueur. Ce cours illustrera donc chaque notion abordée d'exemples et d'exercices, afin de vous enseigner non seulement un langage de programmation, mais aussi de bonnes méthodes de travail.

En résumé, ce tutoriel se composera d'un juste mélange entre théorie et pratique au travers de multiples exemples et exercices d'application qui nécessiterons une investissement personnel et des efforts pour fortifier chaque connaissance acquise.

À qui est destiné ce tutoriel ?

Le tutoriel a pour intention d’être compréhensible par n'importe qui. Que vous soyez un parfait débutant qui souhaite apprendre la programmation sans aucune connaissance au préalable ou que vous soyez un programmeur expérimenté, vous êtes le bienvenu dans ce cours. Les explications seront les plus détaillées possible afin de rendre la lecture accessible à tous.

Cependant, afin que l'apprentissage de ce langage se passe au mieux, certaines qualités sont tout de même requises.

  • De la motivation : la motivation est primordiale dans l'apprentissage d'un langage de programmation, en effet, la manipulation de ce langage nécessite l'assimilation de nombreuses notions, souvent très théoriques et qui sembleront parfois complexes. Il faut donc être bien motivé pour profiter pleinement de cet apprentissage.
  • De la logique : réaliser un programme nécessite un esprit logique développé, bien sûr, ce tutoriel vous guidera dans votre réflexion tout au long de votre apprentissage, mais il faut néanmoins savoir réfléchir par soi-même et être capable de trouver des solutions inventives pour résoudre chaque problème rencontré. En effet, on ne peux pas se contenter d'apprendre par cœur la syntaxe du langage et de simplement recopier mot pour mot plusieurs morceaux de code trouvés par-ci par-là tout au long du cours. Pour obtenir un programme fonctionnel complet, il est nécessaire de réfléchir préalablement à la structure que vous allez donner à celui-ci, ainsi qu'aux nombreuses décompositions que vous serez contraints d'effectuer.
  • De la patience : vous vous apprêtez à apprendre un langage de programmation, et l'assimilation de nombreuses notions plus ou moins complexes vous sera demandée pour la réalisation de programmes qui auraient pu vous paraître très simples de prime abord. Il va donc falloir de la patience pour apprendre, comprendre, s’entraîner, faire des erreurs et les corriger pour enfin pouvoir réaliser les logiciels de vos rêves.
  • De la rigueur : cette qualité est très importante en programmation. Un travail sérieux et régulier tout au long de l'apprentissage du C est primordial, c'est tout ce qui fera la différence entre un bon et un mauvais programmeur.
  • De la passion : le plus important pour suivre ce tutoriel, c'est de prendre plaisir à programmer. Amusez-vous en codant, c'est le meilleur moyen de progresser !

Je tiens aussi à préciser qu’un niveau acceptable en anglais est très fortement recommandé. En effet, beaucoup de cours, de forums, de documentations et autres seront en anglais. Tôt ou tard, vous serez confronté à l'anglais, il faut vous y préparer. Si vous êtes encore étudiant, cela ne vous sera que bénéfique ! Si vraiment l’anglais n’est vraiment pas votre fort, vous pouvez toujours utiliser un dictionnaire pour vous aider.

Dernier point qui concerne les mathématiques : contrairement à la croyance populaire, un bon niveau en maths n’est absolument pas nécessaire pour faire de la programmation. Certes, ça peut aider en développant la logique, mais si les mathématiques ne sont pas votre fort, vous pourrez quand même suivre ce cours sans problèmes.

Allez plus loin

Un des concepts fondamentaux de l’apprentissage de notions informatiques sur Internet est le croisement des sources. Il permet de voir la programmation sous un angle différent. Par exemple, quelques cours de Developpez ou de Wikibook, d’approches différentes, sont à votre entière disposition. N’hésitez pas non plus à lire des livres sur le C, notamment Le C en 20 heures écrit par Eric Berthomier et Daniel Schang ou encore le K&R, écrit par les créateurs du C. C’est un livre très complet qui pourra vous être utile.

Enfin le plus important : n’hésitez pas à programmer tout seul. Faites des exercices, modifiez les codes du tutoriel, regardez ceux des autres, participez à des projets. C’est la meilleure façon de progresser.

La programmation, qu’est-ce que c’est ?

La programmation est une branche de l’informatique qui permet la création de programmes. Votre ordinateur est intégralement contrôlé par des programmes, votre système d’exploitation (Windows, GNU/Linux, Mac OS X, ...) est un programme, votre navigateur web (Internet Explorer, Mozilla Firefox, Opera, ...) est un programme, et cela va de même pour votre logiciel de discussion instantanée, vos jeux vidéos, etc... Les programmes ne sont par ailleurs pas seulement contenus uniquement dans votre ordinateur, et de très nombreux appareils contiennent des programmes, comme par exemple votre lecteur MP3, votre console de jeux, etc.

Un programme est constitué d'une séquence d’instructions, d’ordres, donnés à l’ordinateur afin qu’il exécute des actions. Ces instructions sont généralement assez basiques. On trouve ainsi des opérations mathématiques de base, comme l'addition ou la multiplication, qui font de notre ordinateur une véritable machine à calculer. D’autres instructions plus complexes peuvent exister, comme des opérations permettant de comparer des valeurs, traiter des caractères, etc.

Créer un programme, c’est tout simplement assembler une suite d’instructions de base qui permettront de faire ce que l’on veut. Tous les programmes sont créés ainsi : votre système d'exploitation donne toutes les instructions nécessaires à votre ordinateur, votre logiciel de discussion instantanée donne les instructions nécessaires à l'ordinateur pour discuter avec d'autres gens sur le réseau, votre lecteur multimédia donne des instructions à l’ordinateur pour écouter de la musique ou lire des vidéos, etc.

Petite remarque : on ne peut pas créer d’autres instructions que celles déjà mises en place. Notre ordinateur est conçu, câblé, et peut traiter certaines instructions de bases, préconçues dans ses circuits, sans possibilité d’en inventer d’autres (sauf cas particulier vraiment tordus). Notre ordinateur contient un composant électronique spécialement conçu pour effectuer ces instructions : il s’agit du processeur. Ce qu’il faut retenir, c’est que notre ordinateur contient un circuit, le processeur, qui permet d’effectuer de petits traitements de base qu’on appelle instructions et qui sont la base de tout ce qu’on trouve sur un ordinateur, et que c'est en assemblant tous ces petits traitements de manière très précise que l'on peut obtenir des programmes complexes.

Pour les curieux, il existe un cours sur le fonctionnement d'un ordinateur expliqué depuis zéro.

Ces instructions sont stockées dans notre ordinateur sous la forme de bits, de petites données qui valent soit 0, soit 1. Ainsi, nos instructions ne sont rien d’autre que des suites de 0 et de 1, stockées dans notre ordinateur, et que notre processeur va interpréter comme étant des ordres à effectuer. Ces suites de zéros et un forment ce qu'on appelle le langage machine, qui est difficile à comprendre pour un être humain, communiquer avec l'ordinateur uniquement par le biais de ce langage serait très complexe et extrêmement long. Autant vous dire que créer des programmes de cette façon revient à se tirer une balle dans le pied. En effet, pour maîtriser le langage binaire, il faudrait connaître par cœur des suites de 0 et de 1 totalement abstraites.

Cependant, de nombreux outils servent d'intermédiaire entre le langage humain, bien trop complexe pour une machine, et le langage machine, bien trop abstrait pour un humain. Ces nombreux outils sont les langages de programmation, ceux-ci permettent l'écriture de programmes dans un langage bien plus proche des langages humains tel que le français ou l'anglais, mais néanmoins très logique et structuré afin de permettre une compatibilité avec le langage machine. Grâce à ces langages de programmations, il est possible d'écrire nos programmes sous forme de textes et de nombres en base décimale, c'est-à-dire avec des chiffres de 0 à 9. Il existe de très nombreux langages de programmations, le C est un de ces langages.

Malgré le fait que les langages de programmations soient extrêmement logiques et structurés, ils restent très loin du code binaire interprété par notre processeur. Ainsi, pour que celui-ci puisse suivre les instructions que nous avons programmées, il faut aussi avoir une sorte de traducteur qui fera le lien entre votre langage de programmation et le langage machine. Ce traducteur va ainsi traduire le texte écrit dans un langage de programmation évolué en une suite de zéros et d'un que le processeur peut comprendre. Ainsi vous pourrez commander votre processeur sans pour autant connaître le langage machine.

Pour illustrer, ce code écrit en C (que nous apprendrons à connaître) est quand même largement plus facile à comprendre qu’une suite de 0 et de 1.

#include <stdio.h>

int main(void)
{
    printf("Salut !");
    return 0;
}

Pour vous donner une idée de la difficulté de programmer sans l'intermédiaire d'un langage de programmation, voici ce même programme écrit en langage machine (sous format hexadécimal) :

Programme hexadécimal

0000000 042577 043114 000401 000001 000000 000000 000000 000000 0000020 000002 000003 000001 000000 101440 004004 000064 000000 0000040 010574 000000 000000 000000 000064 000040 000011 000050 0000060 000036 000033 000006 000000 000064 000000 100064 004004 0000100 100064 004004 000440 000000 000440 000000 000005 000000 0000120 000004 000000 000003 000000 000524 000000 100524 004004 0000140 100524 004004 000023 000000 000023 000000 000004 000000 0000160 000001 000000 000001 000000 000000 000000 100000 004004 0000200 100000 004004 002664 000000 002664 000000 000005 000000 0000220 010000 000000 000001 000000 007410 000000 117410 004004 0000240 117410 004004 000430 000000 000434 000000 000006 000000 0000260 010000 000000 000002 000000 007424 000000 117424 004004 0000300 117424 004004 000350 000000 000350 000000 000006 000000 0000320 000004 000000 000004 000000 000550 000000 100550 004004 0000340 100550 004004 000104 000000 000104 000000 000004 000000 0000360 000004 000000 162520 062164 002330 000000 102330 004004 0000400 102330 004004 000054 000000 000054 000000 000004 000000 0000420 000004 000000 162521 062164 000000 000000 000000 000000 0000440 000000 000000 000000 000000 000000 000000 000006 000000 0000460 000020 000000 162522 062164 007410 000000 117410 004004 0000500 117410 004004 000370 000000 000370 000000 000004 000000 0000520 000001 000000 066057 061151 066057 026544 064554 072556 0000540 027170 067563 031056 000000 000004 000000 000020 000000 0000560 000001 000000 047107 000125 000000 000000 000002 000000 0000600 000006 000000 000030 000000 000004 000000 000024 000000 0000620 000003 000000 047107 000125 021435 024626 000000 000001 000024 000031 000000 0014000 101540 004004 000000 000000 000002 000015 000056 000000 0014020 101620 004004 000000 000000 000002 000015 000101 000000 0014040 101720 004004 000000 000000 000002 000015 000127 000000 0014060 120040 004004 000001 000000 000001 000031 000146 000000 0014100 117414 004004 000000 000000 000001 000023 000215 000000 0014120 101760 004004 000000 000000 000002 000015 000231 000000 0014140 117410 004004 000000 000000 000001 000022 000270 000000 0014160 000000 000000 000000 000000 000004 177761 000001 000000 0014200 000000 000000 000000 000000 000004 177761 000277 000000 0014220 102660 004004 000000 000000 000001 000021 000315 000000 0014240 117420 004004 000000 000000 000001 000024 000000 000000 0014260 000000 000000 000000 000000 000004 177761 000331 000000 0014300 117414 004004 000000 000000 000000 000022 000352 000000 0014320 117424 004004 000000 000000 000001 000025 000363 000000 0014340 117410 004004 000000 000000 000000 000022 000406 000000 0014360 120000 004004 000000 000000 000001 000027 000434 000000 0014400 102260 004004 000002 000000 000022 000015 000454 000000 0014420 000000 000000 000000 000000 000040 000000 000510 000000 0014440 101520 004004 000004 000000 001022 000015 000536 000000 0014460 120030 004004 000000 000000 000040 000030 000551 000000 0014500 000000 000000 000000 000000 000022 000000 000573 000000 0014520 120040 004004 000000 000000 000020 000030 000602 000000 0014540 102264 004004 000000 000000 000022 000016 000610 000000 0014560 120030 004004 000000 000000 000020 000030 000625 000000 0014600 000000 000000 000000 000000 000040 000000 000644 000000 0014620 120034 004004 000000 000000 001021 000030 000661 000000 0014640 102314 004004 000004 000000 000021 000017 000700 000000 0014660 000000 000000 000000 000000 000022 000000 000735 000000 0014700 102100 004004 000141 000000 000022 000015 000755 000000 0014720 120044 004004 000000 000000 000020 000031 000762 000000 0014740 101440 004004 000000 000000 000022 000015 000771 000000 0014760 102310 004004 000004 000000 000021 000017 001000 000000 0015000 120040 004004 000000 000000 000020 000031 001014 000000 0015020 102035 004004 000034 000000 000022 000015 001021 000000 0015040 000000 000000 000000 000000 000040 000000 001045 000000 0015060 120040 004004 000000 000000 001021 000030 001061 000000 0015100 000000 000000 000000 000000 000040 000000 001113 000000 0015120 101264 004004 000000 000000 000022 000013 061400 072162 0015140 072163 063165 027146 000143 057537 041512 057522 044514 0015160 052123 057537 062000 071145 063545 071551 062564 057562 0015200 066564 061537 067554 062556 000163 062562 064547 072163 0015220 071145 072137 057555 066143 067157 071545 057400 062137 0015240 057557 066147 061157 066141 062137 067564 071562 060537 0015260 074165 061400 066557 066160 072145 062145 033056 030066 0015300 000070 057537 067544 063537 067554 060542 057554 072144 0015320 071157 057563 072541 057570 064546 064556 060537 071162 0015340 074541 062537 072156 074562 063000 060562 062555 062137 0015360 066565 074555 057400 063137 060562 062555 062137 066565 0015400 074555 064537 064556 057564 071141 060562 057571 067145 0015420 071164 000171 062564 072163 061456 057400 043137 040522 0015440 042515 042537 042116 057537 057400 045137 051103 042537 0015460 042116 057537 057400 064537 064556 057564 071141 060562 0015500 057571 067145 000144 042137 047131 046501 041511 057400 0015520 064537 064556 057564 071141 060562 057571 072163 071141 0015540 000164 043537 047514 040502 057514 043117 051506 052105 0015560 052137 041101 042514 000137 057537 064554 061542 061537 0015600 072563 063137 067151 000151 044537 046524 062137 071145 0015620 063545 071551 062564 052162 041515 067554 062556 060524 0015640 066142 000145 057537 034170 027066 062547 057564 061560 0015660 072137 072550 065556 061056 000170 060544 060564 071537 0015700 060564 072162 070000 064562 072156 040146 043500 044514 0015720 041502 031137 030056 057400 062145 072141 000141 063137 0015740 067151 000151 057537 060544 060564 071537 060564 072162 0015760 057400 063537 067555 057556 072163 071141 057564 000137 0016000 057537 071544 057557 060550 062156 062554 057400 047511 0016020 071537 062164 067151 072537 062563 000144 057537 064554 0016040 061542 071537 060564 072162 066537 064541 040156 043500 0016060 044514 041502 031137 030056 057400 066137 061151 057543 0016100 071543 057565 067151 072151 057400 067145 000144 071537 0016120 060564 072162 057400 070146 064137 000167 057537 071542 0016140 057563 072163 071141 000164 060555 067151 057400 073112 0016160 051137 063545 071551 062564 041562 060554 071563 071545 0016200 057400 052137 041515 042537 042116 057537 057400 052111 0016220 057515 062562 064547 072163 071145 046524 066103 067157 0016240 052145 061141 062554 057400 067151 072151 000000 0016255

Il ne reste plus qu’à utiliser un interprète qui va traduire ce texte (un programme écrit dans notre langage de programmation) vers la langue de l’ordinateur (des suites de 0 et de 1) : le compilateur. Voici un petit schéma qui résume tout ça :

Schéma simplifié de la compilation d'un programme.

Le langage C

Malgré tous ces langages de programmation disponibles, nous allons tout au long de ce cours nous concentrer sur le langage C. Avant d'entamer concrètement la programmation, il me semble important d'évoquer l'histoire de ce langage, ainsi que les caractéristiques et les choix qui nous amènent à l’étudier dans ce cours.

L'histoire du C

Le langage C est né au début des années 1970 dans les laboratoires AT&T aux États-Unis. Son concepteur, Dennis Ritchie, souhaitait améliorer un langage existant, le B, afin de lui adjoindre des nouveautés. En 1973, le C était pratiquement au point, et il commença à être distribué l’année suivante. Son succès était tel auprès des informaticiens que l'ANSI en 1989, puis l’ISO en 1990 décidèrent de le normaliser, c’est-à-dire d’établir les règles officielles du langage. On parle donc de C89 / C ANSI ou bien C90 / C ISO (au choix). D’autres normes sortirent plus tard, en 1999 (on parle de C99) et en 2011 (on parle de C11).

Pourquoi apprendre le C ?

C’est une très bonne question. Après tout, il existe tellement de langages différents, et on peut logiquement se demander pourquoi le C en particulier ? En fait, il y a plusieurs raisons qui peuvent motiver l'apprentissage du C.

  • Sa popularité : il fait partie des langages de programmation les plus utilisés. Il possède une communauté très importante et de nombreux tutoriels et documentations. Vous aurez donc toujours du monde pour vous aider. De plus, il existe beaucoup de programmes et de bibliothèques développés en et pour le C.
  • Sa rapidité : le C est connu pour être un langage très rapide, ce qui en fait un langage de choix pour tout programme où la vitesse d’exécution est cruciale.
  • Sa légèreté : le C est léger, ce qui le rend utile pour les programmes embarqués où la mémoire disponible est faible.
  • Sa portabilité : cela veut dire qu’un programme développé en C marche théoriquement sur n’importe quelle plate-forme. Il faut savoir que le C a été conçu pour la programmation système (pilotes, systèmes d'exploitation, matériel embarqué, etc). Or, les plate-formes étant différentes, il était difficile à l'époque d'avoir un code générique pouvant marcher dans n'importe quel environnement. La volonté des créateurs du C était donc de faire un langage permettant de produire du code portable.

Ce ne sont que quelques arguments parmi tant d'autres, mais se sont à mon goût des raisons plus que suffisantes pour justifier l'apprentissage de ce langage. Bien entendu, le C comporte aussi des défauts. On peut citer la tolérance aux comportements dangereux qui fait que le C demande beaucoup de rigueur pour ne pas tomber dans certains « pièges », le nombre restreint de concepts, qui impose de devoir recoder certains mécanismes qui existent nativement dans d’autres langages, etc. D’ailleurs, si votre but est de développer rapidement des programmes amusants, le C n’est pas du tout adapté à cet usage, et je vous encourage à vous tourner vers d’autres langages tel que le Python.

Le C possède aussi une caractéristique qui peut-être considérée à la fois comme un avantage et comme un défaut : c’est un langage de bas niveau. C'est-à-dire qu’il permet de programmer en étant proche de sa machine, en cherchant à vraiment comprendre ce que l’on fait. C’est à double tranchant : c’est plus difficile et plus long, mais on en apprend beaucoup sur sa machine et on a un grand contrôle sur ce que l’on fait. Cette notion de bas niveau est à opposer aux langages de haut niveau, qui permettent de programmer en faisant abstraction d’un certain nombre de notions primordiales dans le cas d'un langage de bas niveau. Le développement est souvent plus facile et plus rapide, mais en contrepartie on voit moins bien le fonctionnement de la machine. Ces notions de haut et bas niveau sont néanmoins à nuancer, car elles dépendent du langage utilisé et du point de vue du programmeur.

Même si c'est un langage qui ne sera pas abordé au long de ce cours, il me semble important d'évoquer le C++, dont vous avez peut-être déjà entendu parler. C’est un langage de programmation qui a été inventé dans les années 1980 par Bjarne Stroustrup, un collègue de Dennis Ritchie, qui souhaitait rajouter des éléments au C. Bien que très ressemblants à l’époque de sa création, ces deux langages sont aujourd’hui très différents (on ne programme pas et on ne réfléchit pas de la même façon en C qu’en C++). Ne croyez pas qu’il y a un langage meilleur que l’autre. Ils sont simplement différents. Si d’ailleurs votre but est d’apprendre le C++, je vous encourage à le faire directement. Contrairement à ce que l’on pense et dit souvent, il est tout à fait possible d'apprendre le C++ sans passer par l'apprentissage du C.

La norme

Comme précisé plus haut, le C est un langage qui possède des règles. Ces règles ont été définies par des informaticiens professionnels et sont toutes regroupées dans ce que l’on appelle la norme du langage. Chaque fois que l’on a un doute ou que l’on se pose une question, le premier réflexe est de regarder dans la norme ce qui est dit. Bien entendu, la norme n’est pas parfaite, elle ne répond pas à toutes les questions que l'on pourrait se poser et ne précise pas tous les détails dont on pourrait avoir besoin. Néanmoins, elle reste la référence de tout programmeur. Cette norme sert aussi de référence pour les compilateurs. En effet, tous les compilateurs respectent cette norme (en règle générale), afin de parer à différentes interprétations d’un même code.

Cette norme est donc comparable aux règles d’orthographe, de grammaire et de conjugaison de la langue française. Si l'on ne suit plus ces règles et que l'on commence à nous exprimer à notre guise, il devient rapidement impossible de nous comprendre. Le résultat sera le même si chacun commence à interpréter différemment tout ce qu'il entend. La norme sert donc à officialiser tout un tas de règles pour que chaque personne souhaitant communiquer (les programmeurs) puisse être parfaitement compris par chaque interprète (les compilateurs).

Il existe plusieurs versions de la norme : le C89, le C99 et le C11. Dans ce cours, nous avons décidé de nous servir de la norme C89. En effet, même si c’est la plus ancienne et qu’elle semble restrictive à certains, elle permet néanmoins de développer avec n’importe quel compilateur sans problèmes, contrairement aux normes C99 et C11 que tous les compilateurs ne connaissent pas. De plus, il est très facile de passer aux normes plus récentes ensuite. Voici le lien vers le brouillon de cette norme, ce n’est certes pas la version définitive et officielle, mais celle-ci est très chère et le brouillon est largement suffisant pour notre niveau et entièrement gratuit. Bien entendu, cette norme est en anglais.

L’algorithmique

L'algorithmique est très liée à la programmation, et elle constitue même une branche à part des mathématiques. Elle consiste à définir et établir des algorithmes.

Un algorithme peut se définir comme étant une suite finie et non-ambiguë d'opérations permettant de résoudre un problème. En clair, il s'agit d'une succession de calculs qui prennent plusieurs paramètres et renvoient un résultat. Les algorithmes ne sont pas limités à l'informatique, ils existaient en effet bien avant son apparition.

L'intérêt principal des algorithmes est qu'ils sont très utiles lorsqu'ils sont en relation avec des ordinateurs. En effet, ces derniers peuvent exécuter plusieurs millions d'instructions à la seconde, et donc effectuer des calculs très complexes extrêmement rapidement. Illustrons : imaginez que vous deviez trier une liste de 10 nombres dans l'ordre croissant. C'est assez facile et faisable en quelques secondes. Et pour plusieurs milliards de nombres ? C'est parfaitement impossible pour un humain, alors qu'un ordinateur s'en occupera sans aucun problème.

Ce qu'il faut retenir, c'est qu'un algorithme est une suite d'opérations destinée à résoudre un problème donné. Nous aurons l'occasion d'utiliser quelques algorithmes dans ce cours, mais nous ne nous concentrerons pas dessus. Si vous voulez en savoir plus, lisez le tutoriel sur l'algorithmique pour l'apprenti programmeur.

Le pseudo-code

Pour représenter un algorithme indépendamment de tout langage, on utilise ce qu'on appelle un pseudo-code. Il s'agit de la description des étapes de l'algorithme avec une structure proche du langage naturel (dans notre cas le français). Voici un exemple de pseudo-code :

Fonction max (x, y)

    Si x est supérieur à y
        Retourner x
    Sinon
        Retourner y

Fin fonction

Durant ce cours, il y aura plusieurs exercices dans lesquels un algorithme fourni devra être implémenté (traduit) en C. Si vous voulez vous y entraîner davantage tout en suivant ce cours, je vous conseille le site France-IOI qui propose des implémentations de divers algorithmes en plusieurs langages dont le C. Ce qui pourra être un excellent complément.

Voir aussi

  • Wikilivre Programmation C, chapitre Introduction

Langage C

Récupérée de « http://fr.wikiversity.org/w/index.php?title=Langage_C/Introduction_au_C&oldid=448634 »

Catégories :
  • Chapitres de niveau 15 .... http://fr.wikiversity.org/wiki/Catégorie:Chapitres_de_niveau_15
  • Langage C .... http://fr.wikiversity.org/wiki/Catégorie:Langage_C


International Obfuscated C Code Contest — Wikipédia

International Obfuscated C Code Contest

Un article de Wikipédia, l'encyclopédie libre.

Sommaire

  • 1 Origine
  • 2 Méthodes
  • 3 Exemples
  • 4 Aspects considérés
  • 5 Notes et références
    • 5.1 Notes
    • 5.2 Références
  • 6 Annexes
    • 6.1 Articles connexes
    • 6.2 Liens externes

Origine

Les IOCCC ont été lancés par Landon Curt Noll (en) et Larry Bassel. Ils étaient en train de parler du code abominable dont ils devaient faire la maintenance dans leur travail quotidien. Ils décidèrent alors d'organiser un concours du pire code C possible. Dans l'espace autorisé de seulement quelques kilooctets, les participants réussissent à faire des choses compliquées : le vainqueur de 2004 s'est révélé être un système d'exploitation.

Méthodes

Quelques citations des vainqueurs de 2004 incluent :

  • « Pour simplifier les choses, j'ai évité le préprocesseur C et les instructions compliquées comme « if », « for », « do », « while », « switch », et « goto ». »
  • « Nous ne sommes toujours pas sûrs d'avoir un programme utile, mais c'est la première fission atomique vue sur les IOCCC. »
  • « Le programme implémente une ALU de 11 bits dans le préprocesseur. »
  • « Je me suis aperçu que calculer les nombres premiers jusqu'à 1 024 fait s'inclure le programme lui-même plus de 6,8 millions de fois. »

Exemples

Un exemple d'entrée est [ 1 ] , [ 2 ] :

 #define _ -F<00||--F-OO--;
 int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
 {
             _-_-_-_
        _-_-_-_-_-_-_-_-_
     _-_-_-_-_-_-_-_-_-_-_-_
   _-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
   _-_-_-_-_-_-_-_-_-_-_-_-_-_
     _-_-_-_-_-_-_-_-_-_-_-_
         _-_-_-_-_-_-_-_
             _-_-_-_
 }

Ce programme calcule pi en regardant sa propre surface ; une valeur plus précise peut être obtenue en utilisant un programme plus grand (cette entrée a été écrite en C K&R ; telle quelle, elle ne compile pas en C ANSI [ note 1 ] .)

Un autre exemple est un simulateur de vol très astucieux, qui a gagné le IOCCC en 1998 [ 3 ] :

#include                                     <math.h>
#include                                   <sys/time.h>
#include                                   <X11/Xlib.h>
#include                                  <X11/keysym.h>
                                          double L ,o ,P
                                         ,_=dt,T,Z,D=1,d,
                                         s[999],E,h= 8,I,
                                         J,K,w[999],M,m,O
                                        ,n[999],j=33e-3,i=
                                        1E3,r,t, u,v ,W,S=
                                        74.5,l=221,X=7.26,
                                        a,B,A=32.2,c, F,H;
                                        int N,q, C, y,p,U;
                                       Window z; char f[52]
                                    ; GC k; main(){ Display*e=
 XOpenDisplay( 0); z=RootWindow(e,0); for (XSetForeground(e,k=XCreateGC (e,z,0,0),BlackPixel(e,0))
; scanf("%lf%lf%lf",y +n,w+y, y+s)+1; y ++); XSelectInput(e,z= XCreateSimpleWindow(e,z,0,0,400,400,
0,0,WhitePixel(e,0) ),KeyPressMask); for(XMapWindow(e,z); ; T=sin(O)){ struct timeval G={ 0,dt*1e6}
; K= cos(j); N=1e4; M+= H*_; Z=D*K; F+=_*P; r=E*K; W=cos( O); m=K*W; H=K*T; O+=D*_*F/ K+d/K*E*_; B=
sin(j); a=B*T*D-E*W; XClearWindow(e,z); t=T*E+ D*B*W; j+=d*_*D-_*F*E; P=W*E*B-T*D; for (o+=(I=D*W+E
*T*B,E*d/K *B+v+B/K*F*D)*_; p<y; ){ T=p[s]+i; E=c-p[w]; D=n[p]-L; K=D*m-B*T-H*E; if(p [n]+w[ p]+p[s
]== 0|K <fabs(W=T*r-I*E +D*P) |fabs(D=t *D+Z *T-a *E)> K)N=1e4; else{ q=W/K *4E2+2e2; C= 2E2+4e2/ K
 *D; N-1E4&& XDrawLine(e ,z,k,N ,U,q,C); N=q; U=C; } ++p; } L+=_* (X*t +P*M+m*l); T=X*X+ l*l+M *M;
  XDrawString(e,z,k ,20,380,f,17); D=v/l*15; i+=(B *l-M*r -X*Z)*_; for(; XPending(e); u *=CS!=N){
                                   XEvent z; XNextEvent(e ,&z);
                                       ++*((N=XLookupKeysym
                                         (&z.xkey,0))-IT?
                                         N-LT? UP-N?& E:&
                                         J:& u: &h); --*(
                                         DN -N? N-DT ?N==
                                         RT?&u: & W:&h:&J
                                          ); } m=15*F/l;
                                          c+=(I=M/ l,l*H
                                          +I*M+a*X)*_; H
                                          =A*r+v*X-F*l+(
                                          E=.1+X*4.9/l,t
                                          =T*m/32-I*T/24
                                           )/S; K=F*M+(
                                           h* 1e4/l-(T+
                                           E*5*T*E)/3e2
                                           )/S-X*d-B*A;
                                           a=2.63 /l*d;
                                           X+=( d*l-T/S
                                            *(.19*E +a
                                            *.64+J/1e3
                                            )-M* v +A*
                                            Z)*_; l +=
                                            K *_; W=d;
                                            sprintf(f,
                                            "%5d  %3d"
                                            "%7d",p =l
                                           /1.7,(C=9E3+
                              O*57.3)%0550,(int)i); d+=T*(.45-14/l*
                             X-a*130-J* .14)*_/125e2+F*_*v; P=(T*(47
                             *I-m* 52+E*94 *D-t*.38+u*.21*E) /1e2+W*
                             179*v)/2312; select(p=0,0,0,0,&G); v-=(
                              W*F-T*(.63*m-I*.086+m*E*19-D*25-.11*u
                               )/107e2)*_; D=cos(o); E=sin(o); } }

Aspects considérés

Quelques aspects remarquables des entrées incluent :

  • La forme du listing, qui peut ressembler à des images, du texte, etc. ;
  • Des redéfinitions du préprocesseur pour rendre le code plus difficile à lire ;
  • Du code auto-modifiant ;
  • Le pire abus des règles : à plusieurs reprises, une entrée a été soumise qui était si évidemment absurde qu'elle a entraîné une redéfinition des règles pour les années à venir. C'est un très grand honneur. Un exemple est le plus petit programme qui s'affiche lui-même. L'entrée était un programme de taille zéro, qui, quand il était lancé, affichait zéro octets à l'écran (ça demande un usage créatif du makefile pour tomber juste).

Plusieurs des entrées précédentes sont notables pour avoir causé des pannes de compilateurs (plus particulièrement celui de Visual Studio).


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières Plier Déplier

  • Introduction
    • I. Introduction
      • I-A. Historique
      • I-B. Caractéristiques du langage C
        • I-B-1. Universalité
        • I-B-2. Concision, souplesse
        • I-B-3. Puissance
        • I-B-4. Portabilité
        • I-B-5. Omniprésence
      • I-C. A propos de ce document
      • I-D. Remerciements
  • Instruction, programme et fonction
    • II. Instruction, programme et fonction
      • II-A. Structure générale d'un programme C
        • II-A-1. Un premier programme
        • II-A-2. Les commentaires
        • II-A-3. Sensibilité à la casse
        • II-A-4. Le format libre
      • II-B. Du fichier source à l'exécutable
        • II-B-1. La compilation
        • II-B-2. Applications consoles
        • II-B-3. Manipulations avec Visual C++ et Code::Blocks
          • II-B-3-a. Avec Visual C++ 6
          • II-B-3-b. Avec Visual Studio .NET
          • II-B-3-c. Avec Code::Blocks 1.0
      • II-C. Les fonctions
        • II-C-1. Introduction
        • II-C-2. Exemple avec une fonction "mathématique"
        • II-C-3. Exemple avec une "procédure"
        • II-C-4. Remarques
      • II-D. Les macros
        • II-D-1. Le préprocesseur
        • II-D-2. Définition
      • II-E. Exercices
        • II-E-1. La lettre X
        • II-E-2. Périmètre d'un rectangle
  • Expressions et instructions
    • III. Expressions et instructions
      • III-A. Introduction aux types de données du langage C
        • III-A-1. Les types de base du langage C
        • III-A-2. Règle d'écriture des constantes littérales
          • Nombres entiers
          • Nombres flottants
          • Caractères et chaînes de caractères
        • III-A-3. Spécification de format dans printf
          • III-A-3-a. Généralités
          • III-A-3-b. Les "options"
          • III-A-3-c. Le type entier
          • III-A-3-d. Le type flottant
        • III-A-4. Les variables et les constantes
        • III-A-5. Définition de nouveaux types
      • III-B. Les pointeurs
        • III-B-1. Définition
        • III-B-2. Saisir des données tapées au clavier avec la fonction scanf
        • III-B-3. Exemple de permutation des contenus de deux variables
      • III-C. Les expressions
        • III-C-1. Introduction
        • III-C-2. lvalue et rvalue
        • III-C-3. Opérations usuelles
          • III-C-3-a. Les opérateurs arithmétiques courants
          • III-C-3-b. Les opérateurs de comparaison
          • III-C-3-c. Les opérateurs logiques
        • III-C-4. L'opérateur virgule
        • III-C-5. Taille des données. L'opérateur sizeof
        • III-C-6. Les opérateurs d'incrémentation et de décrémentation
        • III-C-7. Expressions conditionnelles
        • III-C-8. Autres opérateurs d'affectation
        • III-C-9. Ordre de priorité des opérateurs
      • III-D. Considérations liées à la représentation binaire
        • III-D-1. Généralités
        • III-D-2. Les caractères
        • III-D-3. Dépassement de capacité
      • III-E. La conversion de type
        • III-E-1. Conversion implicite
        • III-E-2. Conversion explicite (cast)
      • III-F. Les instructions
        • III-F-1. Introduction
        • III-F-2. Bloc d'instructions
        • III-F-3. L'instruction if
        • III-F-4. L'instruction do
        • III-F-5. L'instruction while
        • III-F-6. L'instruction for
        • III-F-7. Les instructions switch et case
        • III-F-8. L'instruction break
        • III-F-9. L'instruction continue
        • III-F-10. L'instruction return
        • III-F-11. L'instruction vide
      • III-G. Exercices
        • III-G-1. Valeur absolue
        • III-G-2. Moyenne
        • III-G-3. L'heure dans une minute
        • III-G-4. Rectangle
        • III-G-5. Triangle isocèle
        • III-G-6. Somme
  • Tableaux, pointeurs et chaînes de caractères
    • IV. Tableaux, pointeurs et chaînes de caractères
      • IV-A. Les tableaux
        • IV-A-1. Définition
        • IV-A-2. Initialisation
        • IV-A-3. Création d'un type « tableau »
        • IV-A-4. Les tableaux à plusieurs dimensions
        • IV-A-5. Calculer le nombre d'éléments d'un tableau
      • IV-B. Les pointeurs
        • IV-B-1. Les tableaux et les pointeurs
        • IV-B-2. L'arithmétique des pointeurs
        • IV-B-3. Pointeurs constants et pointeurs sur constante
        • IV-B-4. Pointeurs génériques
        • IV-B-5. Exemple avec un tableau à plusieurs dimensions
        • IV-B-6. Passage d'un tableau en argument d'une fonction
      • IV-C. Les chaînes de caractères
        • IV-C-1. Chaîne de caractères
        • IV-C-2. Longueur d'une chaîne
        • IV-C-3. Représentation des chaînes de caractères en langage C
        • IV-C-4. Les fonctions de manipulation de chaîne
          • strcpy, strncpy
          • strcat, strncat
          • strlen
          • strcmp, strncmp
        • IV-C-5. Fusion de chaînes littérales
      • IV-D. Exercices
        • IV-D-1. Recherche dans un tableau
        • IV-D-2. Calcul de la moyenne
        • IV-D-3. Manipulation de chaînes
  • Les entrées/sorties en langage C
    • V. Les entrées/sorties en langage C
      • V-A. Introduction
      • V-B. Les fichiers
      • V-C. Les entrée et sortie standards
      • V-D. Exemple : lire un caractère, puis l'afficher
      • V-E. Saisir une chaîne de caractères
      • V-F. Lire une ligne avec fgets
      • V-G. Mécanisme des entrées/sorties en langage C
        • V-G-1. Le tamponnage
          • V-G-1-a. Les tampons d'entrée/sortie
          • V-G-1-b. Les modes de tamponnage
        • V-G-2. Lire de manière sûre des données sur l'entrée standard
  • L'allocation dynamique de mémoire
    • VI. L'allocation dynamique de mémoire
      • VI-A. Les fonctions malloc et free
      • VI-B. La fonction realloc
      • VI-C. Exercices
        • VI-C-1. Calcul de la moyenne (version 2)
        • VI-C-2. Recherche dans un tableau (version 2)
    • VI. Solutions des exercices
      • VI-A. La lettre X (II-E-1)
      • VI-B. Périmètre d'un rectangle (II-E-2)
      • VI-C. Valeur absolue (III-G-1)
      • VI-D. Moyenne (III-G-2)
      • VI-E. L'heure dans une minute (III-G-3)
      • VI-F. Rectangle (III-G-4)
      • VI-G. Triangle isocèle (III-G-5)
      • VI-H. Somme (III-G-6)
      • VI-I. Recherche dans un tableau (IV-D-1)
      • VI-J. Calcul de la moyenne (IV-D-2)
      • VI-K. Manipulation de chaînes (IV-D-3)
      • VI-L. Calcul de la moyenne (version 2) (VI-C-1)
      • VI-M. Recherche dans un tableau (version 2) (VI-C-2)
  • Solutions des exercices
    • Conclusion
      • VIII. Conclusion

Ce tutoriel va vous apprendre les concepts de base du langage C. Il n'est ni une référence ni une définition du langage mais a été écrit dans le but d'aider le lecteur à le prendre en main avant d'étudier les concepts avancés de ce langage. Bonne lecture.


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • I. Introduction
    • I-A. Historique
    • I-B. Caractéristiques du langage C
      • I-B-1. Universalité
      • I-B-2. Concision, souplesse
      • I-B-3. Puissance
      • I-B-4. Portabilité
      • I-B-5. Omniprésence
    • I-C. A propos de ce document
    • I-D. Remerciements

I. Introduction

I-A. Historique

L'histoire du langage C est intimement liée à celle du système d'exploitation UNIX. En 1969, Ken Thompson, qui travaillait pour Bell Laboratories, mit au point sur un DEC PDP-7 un système d'exploitation semblable à Multics mais plus simple et plus modeste. Ce système reçut par la suite le nom d'Unics (Uniplexed Information and Computing System) qui ne tarda pas à être définitivement changé en UNIX.

A l'époque, le seul langage vraiment adapté à l'écritute de systèmes d'exploitation était le langage d'assemblage. Ken Thompson développa alors un langage de plus haut niveau (c'est-à-dire plus proche du langage humain), le langage B (dont le nom provient de BCPL, un sous-ensemble du langage CPL, lui-même dérivé de l'Algol, un langage qui fut populaire à l'époque), pour faciliter l'écriture d'UNIX. C'était un langage faiblement typé (un langage non typé, par opposition à un langage typé, est un langage qui manipule les objets sous leur forme binaire, sans notion de type (caractère, entier, réel, ...)) et trop dépendant du PDP-7 pour permettre de porter UNIX sur d'autres machines. Alors Denis Ritchie et Brian Kernighan, de Bell Laboratories également, améliorèrent le langage B pour donner naissance au langage C. En 1973, UNIX fut réécrit entièrement en langage C. Pendant 5 ans, le langage C fut limité à l'usage interne de Bell jusqu'au jour ou Brian Kernighan et Denis Ritchie publièrent une première définition du langage dans un ouvrage intitulé The C Programming Language. Ce fut le début d'une révolution dans le monde de l'informatique.

Grâce à sa puissance, le langage C devint rapidement très populaire et en 1983, l'ANSI (American National Standards Institute) décida de le normaliser en ajoutant également quelques modifications et améliorations, ce qui donna naissance en 1989 au langage tel que nous le connaissons aujourd'hui. L'année suivante, le langage C a été adopté par l'ISO (International Organization for Standardization) et actuellement, par l'ISO et l'IEC (International Electrotechnical Commission), et a connu au fil du temps certaines révisions (dans l'immense majorité des cas toujours compatibles avec les versions antérieures) dont les plus importantes sont celles de 1990 (adoption par l'ISO), de 1995 (amélioration du support du développement de programmes internationnaux) et de 1999 (ajout d'un nombre important de nouvelles fonctionnalités). La version de 1999 est cependant actuellement encore peu diffusée c'est pourquoi dans ce document, nous nous placerons dans l'apprentissage des versions de 1990 et 1995.

I-B. Caractéristiques du langage C

I-B-1. Universalité

Langage de programmation par excellence, le C n'est pas confiné à un domaine particulier d'applications. En effet, le C est utilisé dans l'écriture

  • de systèmes d'exploitations (comme Windows, UNIX et Linux) ou de machines virtuelles (JVMs, Runtimes et Frameworks .NET, logiciels de virtualisation, etc.)
  • de logiciels de calcul scientifique, de modélisation mathématique ou de CFAO (Matlab, R, Labview, Scilab, etc.)
  • de bases de données (MySQL, Oracle, etc.)
  • d'applications en réseau (applications intranet, internet, etc.)
  • de jeux vidéo (notamment avec OpenGL ou la SDL et/ou les différents moteurs (de jeu, physique ou 3D) associés))
  • d'assembleurs, compilateurs, débogueurs, interpréteurs, de logiciels utilitaires et dans bien d'autres domaines encore.

Oui, le C permet tout simplement de tout faire, ce qui lui a valu, à juste titre, l'appelation courante "the God's programming language" (le langage de programmation de Dieu). A cause de son caractère proche du langage de la machine, le C est cependant peu productif, ce qui signifie qu'il faut souvent écrire beaucoup pour faire peu. C'est donc le prix à payer pour l'utilisation de ce langage surpuissant !

I-B-2. Concision, souplesse

C'est un langage concis, très expressif, et les programmes écrits dans ce langage sont très compacts grâce à un jeu d'opérateurs puissant.

I-B-3. Puissance

Le C est un langage de haut niveau mais qui permet d'effectuer facilement des opérations de bas niveau et d'accéder aux fonctionnalités du système, ce qui est la plupart du temps impossible ou difficile à réaliser dans les autres langages de haut niveau. C'est d'ailleurs ce qui fait l'originalité du C, et aussi la raison pour laquelle il est autant utilisé dans les domaines où la performance est cruciale comme la programmation système, le calcul numérique ou encore l'informatique embarquée.

I-B-4. Portabilité

C'est un langage qui ne dépend d'aucune plateforme matérielle ou logicielle, c'est-à-dire qui est entièrement portable. De plus, de par sa simplicité, écrire un compilateur C pour un processeur donné n'est pas beaucoup plus compliqué que d'écrire un assembleur pour ce même processeur. Ainsi, là où l'on dispose d'un assembleur pour programmer, on dispose aussi généralement d'un compilateur C, d'où l'on dit également que le C est un "assembleur portable".

I-B-5. Omniprésence

La popularité du langage mais surtout l'élégance des programmes écrits en C est telle que son style et sa syntaxe ont influencé de nombreux langages :

  • C++ et Objective C sont directement descendus du C (on les considère souvent comme des extensions du C)
  • C++ a influencé Java
  • Java a influencé JavaScript
  • PHP est un mélange de C et de langage de shell-scripting sous Linux
  • C# est principalement un mix de C++ et de Java, deux langages de style C, avec quelques influences fonctionnelles d'autres langages comme Delphi et Visual Basic (le # de C# vient-il dit-on de deux "++" superposés)
  • Et on risque d'y passer la journée ...

Connaître le C est donc très clairement un atout majeur pour quiconque désire apprendre un de ces langages.

I-C. A propos de ce document

Ce document contient des cours et des exercices. Chaque fois que vous tombez sur des exercices, traitez-les avant de passer au chapitre ou au paragraphe suivant.


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • II. Instruction, programme et fonction
    • II-A. Structure générale d'un programme C
      • II-A-1. Un premier programme
      • II-A-2. Les commentaires
      • II-A-3. Sensibilité à la casse
      • II-A-4. Le format libre
    • II-B. Du fichier source à l'exécutable
      • II-B-1. La compilation
      • II-B-2. Applications consoles
      • II-B-3. Manipulations avec Visual C++ et Code::Blocks
        • II-B-3-a. Avec Visual C++ 6
        • II-B-3-b. Avec Visual Studio .NET
        • II-B-3-c. Avec Code::Blocks 1.0
    • II-C. Les fonctions
      • II-C-1. Introduction
      • II-C-2. Exemple avec une fonction "mathématique"
      • II-C-3. Exemple avec une "procédure"
      • II-C-4. Remarques
    • II-D. Les macros
      • II-D-1. Le préprocesseur
      • II-D-2. Définition
    • II-E. Exercices
      • II-E-1. La lettre X
      • II-E-2. Périmètre d'un rectangle

II. Instruction, programme et fonction

II-A. Structure générale d'un programme C

II-A-1. Un premier programme

Nous allons écrire un programme qui affiche Hello, world en langage C.

hello.c
Sélectionnez
#include <stdio.h>

int main()
{
    printf("Hello, world\n");
    return 0;
}

Voici la sortie de ce programme (c'est-à-dire, ce que nous, utilisateurs, verrons affiché sur le périphérique de sortie standard) :

Sélectionnez

Hello, world

Le caractère '\n' dans la chaîne de caractères "Hello, world\n" représente le caractère de fin de ligne. Il permet d'insérer une nouvelle ligne (new line).

En langage C, la « brique » qui permet de créer un programme est la fonction. Un programme écrit en C est donc constitué d'une ou plusieurs fonctions, une fonction étant généralement composée d'une ou plusieurs instructions, chaque instruction élémentaire devant se terminer par un point-virgule.

printf est une fonction qui permet d'afficher du texte sur la sortie standard, par défaut l'écran. main est également une fonction, c'est celle qui sera automatiquement appelée à l'exécution. On l'appelle le point d'entrée du programme ou encore la fonction principale. Ici, elle est seulement composée de deux instructions :

  1. printf("Hello, world\n");
  2. return 0;

Selon la norme officielle du langage C, main est une fonction qui doit retourner un entier (int). Chez de nombreux systèmes (dont Windows et UNIX), cet entier est appelé le code d'erreur de l'application. En langage C, bien que cela ne soit pas forcément le cas pour le système d'exploitation, on retourne 0 pour dire que tout s'est bien passé.

Le langage C impose (à quelques exceptions près que nous verrons plus bas) qu'une fonction doit avoir été déclarée avant de pouvoir être utilisé (nous verrons plus tard ce que c'est qu'une déclaration). Dans notre exemple, puisque nous utilisons la fonction printf, nous devons tout d'abord la déclarer. Le moyen le plus simple de le faire est d'inclure le fichier stdio.h, qui contient entre autres la déclaration de cette fonction, à l'aide de la directive include :

Sélectionnez

#include <stdio.h>

II-A-2. Les commentaires

On peut insérer des commentaires n'importe où dans du code C à l'aide des délimiteurs /* et */. Les commentaires permettent de rendre les programmes plus lisibles. Voici un exemple d'utilisation de commentaires :

hello.c
Sélectionnez
#include <stdio.h>

int main()
{
    /* Ce programme affiche "Hello, world" */
    printf("Hello, world\n");
    return 0;
}

II-A-3. Sensibilité à la casse

Le C est sensible à la casse : il fait la distinction entre les majuscules et les minuscules. Printf et printf sont deux choses complètement différentes et qui n'ont strictement rien à voir ...

II-A-4. Le format libre

En langage C, les espaces (y compris les tabulations et les retours à la ligne) peuvent être utilisées à volonté (ou pas du tout) avant ou après un séparateur (# < > ( ) { } ; …). On traduit ce fait en disant que le C est un langage au format libre. Une chaîne ("...") doit cependant toujours tenir sur une seule ligne (nous reviendrons sur ce point plus tard). Ainsi, notre programme qui affiche "Hello, world" aurait également pu s'écrire :

Sélectionnez
#   include    <stdio.h>
int
main ( )
{   printf    (
        "Hello, world\n"
    );return

    0

;}

Dans certains cas, les espaces peuvent cependant être utilisés de manière précise. Par exemple, avec :

Sélectionnez

#include < stdio.h >

Vous demandez d'inclure le fichier <espace>stdio.h<espace> (qui a de fortes chances de ne pas exister, ce qui génèrerait des erreurs à la compilation ...) et non le fichier stdio.h !

II-B. Du fichier source à l'exécutable

II-B-1. La compilation

Avant de définir la compilation, il y a deux expressions qu'il faut déjà avoir défini. Il s'agit de "fichier source" et "fichier exécutable".

  • Un fichier source est un fichier contenant, dans le cas qui nous intéresse, des lignes de code écrits en C. Le code contenu dans le fichier source est appelé du code source. Les fichiers sources C portent généralement l'extension ".c" (par exemple : hello.c).

  • Un fichier exécutable est un fichier contenant, entre autres, du code directement exécutable par le processeur. Ce code est appelé code machine. L'extension portée par les fichiers exécutables varient selon le système. Sous Windows par exemple, ils portent généralement l'extension ".exe" (par exemple : hello.exe). Sous Linux, ils ne portent généralement pas d'extension.

La compilation c'est, en première approximation, le processus pendant lequel le fichier source (qui ne contient que du texte) est transformé en fichier exécutable (qui contient du code compréhensible par le processeur). Une définition plus précise sera donnée dans les cours plus avancés.

Puisque les programmes C doivent être compilés avant de pouvoir être exécutés, on dit que le C est un langage compilé. Le programme qui effectue la compilation est appelé compilateur.

Nombreux programmeurs utilisent cependant un EDI (Environnement de Développement Intégré) au lieu d'un simple compilateur pour compiler leurs programmes. Un EDI est un logiciel qui intègre un éditeur de texte pour taper les codes sources, un compilateur pour les traduire en exécutable ainsi que d'autres outils aidant à la mise au point et à la distribution des programmes. Il existe de nombreux EDIs pour le langage C. Certains sont gratuits (Code::Blocks Studio, Visual C++ Express, etc.), d'autres payants (CodeGear RAD Studio, Microsoft Visual Studio, etc.). Téléchargez ou achetez et installez le logiciel qui vous convient donc avant de continuer. Pour les manipulations pratiques, nous supposerons dans la suite que nous travaillons sous Windows, mais que cela ne vous empêche de lire ce tutoriel même si vous utilisez un autre système.

II-B-2. Applications consoles

Le C voulant être un langage entièrement portable, la création d'interfaces graphiques n'est pas supportée en standard par le langage. En effet, la notion d'interface graphique n'existe pas forcément dans tous les systèmes. Dans les systèmes proposant un environnement graphique, comme Windows par exemple, les applications qui en sont dépourvues sont souvent appelées applications consoles. Ces applications, quand elles sont lancées, s'exécutent dans une fenêtre (la fameuse "console") dans laquelle l'utilisateur n'utilise que le clavier pour commander le programme). Dans ce tutoriel, c'est toujours ce type d'applications que allons développer.

II-B-3. Manipulations avec Visual C++ et Code::Blocks

II-B-3-a. Avec Visual C++ 6
  1. Lancez Visual C++.

  2. Créez un projet d'application console que vous allez nommer helloworld en choisissant File > New > Project > Win32 Console Application. Un dossier nommé helloworld est alors créé. Ce sera votre répertoire par défaut. Deux fichiers, entre autres, sont également créés : helloworld.prj (votre projet) et helloworld.dsw (votre espace de travail).

  3. Choisissez An empty project pour qu'aucun fichier ne soit automatiquement ajouté à notre projet.

  4. Ajoutez au projet un nouveau fichier que vous allez nommer hello.c avec la commande Project > Add to Project > New > C++ source file. Nommez-le bien hello.c car si vous omettez l'extension, VC6 va automatiquement ajouter l'extension .cpp et vous aurez donc un fichier source C++ (hello.cpp) au lieu d'un fichier source C, à moins bien sûr que c'est justement ce que vous cherchiez ...

  5. Dans l'explorateur de projet (normalement c'est sur votre gauche), cliquez sur File View pour afficher une vue des fichiers qui constituent votre projet. Ouvrez ensuite le dossier Source Files puis double cliquez sur hello.c pour l'ouvrir dans l'éditeur.

  6. Saisissez maintenant le code puis compilez avec la commande Build > Build helloworld.exe (F7). Le fichier helloworld\Debug\helloworld.exe est alors créé.

  7. Pour tester votre programme sans quitter VC, choisissez Build > Execute helloworld.exe (Ctrl + F5). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.
II-B-3-b. Avec Visual Studio .NET

La procédure est presque les même que sous VC6. Créez un nouveau projet d'application Windows (Win32 Project) puis dans l'étape Applications Settings de l'assistant, choissez Console Application puis Empty Project. Compilez puis testez.

II-B-3-c. Avec Code::Blocks 1.0
  1. Lancez Code::Blocks.

  2. Créez un nouveau projet d'application console que vous aller nommer helloworld en choisissant File > New Project > Console Application. Enregistrez-le dans un répertoire de votre choix qui sera alors votre répertoire par défaut. Avant de valider, cochez l'option Do not create any files afin qu'aucun fichier ne soit automatiquement ajouté à notre projet. Une fois que vous avez validé, un fichier helloworld.cbp (votre projet) sera créé dans le répertoire de votre projet.

  3. Créez un nouveau fichier que vous allez nommer hello.c avec la commande File > New File. Acceptez que ce fichier soit ajouté au projet.

  4. Saisissez maintenant le code puis compilez avec la commande Build > Build (Ctrl + F9). Le fichier helloworld.exe est alors créé.

  5. Pour tester votre programme sans quitter Code::Blocks, choisissez Build > Run (Ctrl + F10). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.

II-C. Les fonctions

II-C-1. Introduction

Comme nous l'avons déjà dit plus haut, les fonctions sont les briques avec lesquelles on construit un programme en langage C. La vie d'un programmeur C se résume donc à créer puis utiliser des fonctions. Vous allez adorer ! Nous allons donc maintenant voir de plus près ce que c'est qu'une fonction.

II-C-2. Exemple avec une fonction "mathématique"

En mathématiques, on définit une fonction comme suit :

f(x) = x² - 3

Cela signifie que f est une fonction qui reçoit en argument un réel x et qui retourne un réel : x2 - 3.

Ecrivons une fonction C que nous appellerons f, qui reçoit en argument un entier x et qui retourne également un entier : x2 - 3.

int f(int x)
{
    return x*x - 3;
}

Le code ci-dessus constitue ce qu'on appelle la définition ou encore l'implémentation de la fonction f. Voyons maintenant un exemple d'utilisation de cette fonction.

#include <stdio.h>

int f(int); /* declaration de la fonction f */

int main()
{
    int x = 4;
    printf("f(%d) = %d\n", x, f(x));
    return 0;
}

int f(int x)
{
    return x*x - 3;
}

La ligne :

int f(int);

tient lieu de déclaration de la fonction f. Sans le point-virgule, on a ce qu'on appelle le prototype ou la signature de la fonction f. Ici, le prototype (la signature) indique que "f est une fonction qui nécessite en argument un int (int f(int)) et qui retourne un int (int f(int))". Un prototype suivi d'un point-virgule constitue ce qu'on appelle une déclaration (mais ce n'est pas la seule forme possible de déclaration d'une fonction).

Attention ! Nous avons vu jusqu'ici qu'une instruction élémentaire se termine par un point-virgule. Cela ne signifie en aucun cas que tout ce qui se termine par un point-virgule est donc une « instruction ». Une déclaration est une déclaration, pas une instruction. Nous définirons ce que c'est exactement une instruction encore plus loin.

Le %d dans la chaîne passée en premier argument de printf est ce qu'on appelle un spécificateur de format. Il renseigne sur la manière dont nous souhaitons afficher le texte. Ici, on veut afficher les nombres 4 et 13 (f(4)). Nous disons donc à printf d'utiliser le format « nombre entier » (%d) pour les afficher. Le premier %d correspond au format que nous voulons utiliser pour afficher x et le deuxième pour f(x). Nous y reviendrons un peu plus loin.

Sachez également que la variable x dans la fonction main n'a absolument rien à voir avec la variable x en paramètre de la fonction f. Chaque fonction peut avoir ses propres variables et ignore complètement ce qui se passe chez les autres fonctions. On vient ainsi d'introduire le concept de "variables locales" que nous étudierons également plus tard.

II-C-3. Exemple avec une "procédure"

Certains langages de programmation (Pascal, VB (Visual Basic), etc.) font la distinction entre "fonction" et "procédure". En langage C et ses dérivés, il n'y a pas cette distinction. Ce que chez certains langages on appelle procédure est en effet ni plus ni moins qu'une fonction qui effectue un traitement mais qui ne retourne pas de valeur, c'est-à-dire une fonction qui ne retourne rien. En langage C, ce "rien" est indiqué par le mot-clé void (terme anglais signifiant "vide"). Voici un exemple de fonction qui ne requiert aucun argument et qui ne retourne rien : une fonction qui affiche "Bonjour." 3 fois (en 3 lignes).

#include <stdio.h>

void Bonjour3Fois(void);

int main()
{
    /* Ce programme affiche Bonjour 6 fois */
    Bonjour3Fois();
    Bonjour3Fois();
    return 0;
}

void Bonjour3Fois(void)
{
    printf("Bonjour.\n");
    printf("Bonjour.\n");
    printf("Bonjour.\n");
}

II-C-4. Remarques

Dans une déclaration on peut :

  • Mettre le nom des arguments de la fonction (bon uniquement pour la déco et rien d'autre). Par exemple :
    exemple de déclaration d'une fonction
    Sélectionnez

int Surface(int Longueur, int largeur);
  • Ne pas préciser les arguments de la fonction (déconseillé). Dans ce cas, il faut aller à la définition de la fonction pour connaître les arguments qu'elle requiert. Attention ! Ne pas préciser les arguments d'une fonction n'est pas la même chose que déclarer une fonction qui ne requiert aucun argument. Exemple :
    exemple de déclaration incomplète d'une fonction (déconseillé)
    Sélectionnez

int Surface(); /* Surface est une fonction, point. Voir sa definition pour plus de details. */
    exemple de déclaration d'une fonction qui ne requiert aucun argument
    Sélectionnez

int Surface(void);

De plus on peut :

  • Ne pas déclarer une fonction retournant un int (déconseillé).

  • La déclaration d'une fonction n'est nécessaire que lorsque son utilisation précède sa définition. En effet, une fois définie, la fonction est entièrement connue et donc n'a plus besoin d'être déclarée. Il est cependant toujours conseillé de ne définir une fonction qu'après son utilisation (ce qui requiert donc une déclaration) ne serait-ce que pour la lisibilité du programme (en effet c'est le programme qu'on veut voir à première vue, pas les petits détails).

Dans une définition:

  • On peut ne pas préciser le type de retour d'une fonction (déconseillé). Dans ce cas celle-ci est supposée retourner un int.

  • Si la fonction a déjà été déclarée à l'aide d'une déclaration complète (c'est-à-dire avec prototype), une paire de parenthèses vide signifie que la fonction n'accepte aucun argument. Sinon, les parenthèses vides signifient que les arguments requis par la fonction ne sont pas renseignés. Cette manière de définir une fonction est à éviter sauf pour des fonctions qui ne sont jamais appelées depuis le programme comme main() par exemple.

II-D. Les macros

II-D-1. Le préprocesseur

Avant d'être effectivement compilés, les fichiers sources C sont traités par un préprocesseur qui résout certaines directives qui lui sont données comme l'inclusion de fichier par exemple. Le préprocesseur, quoi qu'étant un programme à priori indépendant du compilateur, est un élément indispensable du langage.

Une directive donnée au préprocesseur commence toujours par #. Nous avons déjà rencontré la directive include qui permet d'inclure un fichier. La directive define permet de définir des macros.

II-D-2. Définition

Une macro, dans sa forme la plus simple, se définit de la manière suivante :

#define <macro> <le texte de remplacement>

Par exemple :

#define N 10

commande au préprocesseur de remplacer toutes les occurrences de le lettre N par 10. Bien sûr il n'y a pas que les lettres et les nombres qu'on peut utiliser, rien ne nous empêche par exemple d'écrire :

#define PLUS +

Ainsi, toutes les occurrences de PLUS seront remplacés par +.

Dans certaines circonstances, les macros peuvent également avantageusement remplacer les fonctions. Par exemple :

#define carre(x) x * x

Dans ce cas, une expression telle que

carre(3)

sera remplacé par :

3 * 3

Si cela semble fonctionner à première vue, voyons un peu ce qu'on obtiendrait avec :

carre(1 + 1)

Et bien, on obtiendrait :

1 + 1 * 1 + 1

soit 1 + 1 + 1 soit 3 car la multiplication est prioritaire par rapport à l'addition. Nous verrons plus tard l'ordre de priorité des opérateurs. C'est pourquoi on utilise généralement beaucoup de parenthèses dans le corps d'une macro. Ainsi, dans l'exemple précédent, on aurait du par exemple écrire :

#define carre(a) ((a) * (a))

Ce qui nous donnerait pour carre(1 + 1) par exemple :

((1 + 1) * (1 + 1))

II-E. Exercices

II-E-1. La lettre X

Ecrire un programme qui dessine la lettre dans une grille de dimensions 5 x 5 en utilisant des espaces et le caractère '*'. Cela signifie qu'on veut avoir comme sortie :

*   *
 * *
  *
 * *
*   *

II-E-2. Périmètre d'un rectangle

Ecrire une fonction qui permet d'avoir le périmètre d'un rectangle de longueur et largeur données en arguments et utiliser cette fonction dans un programme arbitraire pour afficher le périmètre d'un rectangle de dimensions 100 x 60. Exemple de sortie :

Le perimetre d'un rectangle de longueur 100 et de largeur 60 est 320.


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • III. Expressions et instructions
    • III-A. Introduction aux types de données du langage C
      • III-A-1. Les types de base du langage C
      • III-A-2. Règle d'écriture des constantes littérales
        • Nombres entiers
        • Nombres flottants
        • Caractères et chaînes de caractères
      • III-A-3. Spécification de format dans printf
        • III-A-3-a. Généralités
        • III-A-3-b. Les "options"
        • III-A-3-c. Le type entier
        • III-A-3-d. Le type flottant
      • III-A-4. Les variables et les constantes
      • III-A-5. Définition de nouveaux types
    • III-B. Les pointeurs
      • III-B-1. Définition
      • III-B-2. Saisir des données tapées au clavier avec la fonction scanf
      • III-B-3. Exemple de permutation des contenus de deux variables
    • III-C. Les expressions
      • III-C-1. Introduction
      • III-C-2. lvalue et rvalue
      • III-C-3. Opérations usuelles
        • III-C-3-a. Les opérateurs arithmétiques courants
        • III-C-3-b. Les opérateurs de comparaison
        • III-C-3-c. Les opérateurs logiques
      • III-C-4. L'opérateur virgule
      • III-C-5. Taille des données. L'opérateur sizeof
      • III-C-6. Les opérateurs d'incrémentation et de décrémentation
      • III-C-7. Expressions conditionnelles
      • III-C-8. Autres opérateurs d'affectation
      • III-C-9. Ordre de priorité des opérateurs
    • III-D. Considérations liées à la représentation binaire
      • III-D-1. Généralités
      • III-D-2. Les caractères
      • III-D-3. Dépassement de capacité
    • III-E. La conversion de type
      • III-E-1. Conversion implicite
      • III-E-2. Conversion explicite (cast)
    • III-F. Les instructions
      • III-F-1. Introduction
      • III-F-2. Bloc d'instructions
      • III-F-3. L'instruction if
      • III-F-4. L'instruction do
      • III-F-5. L'instruction while
      • III-F-6. L'instruction for
      • III-F-7. Les instructions switch et case
      • III-F-8. L'instruction break
      • III-F-9. L'instruction continue
      • III-F-10. L'instruction return
      • III-F-11. L'instruction vide
    • III-G. Exercices
      • III-G-1. Valeur absolue
      • III-G-2. Moyenne
      • III-G-3. L'heure dans une minute
      • III-G-4. Rectangle
      • III-G-5. Triangle isocèle
      • III-G-6. Somme

    • III. Expressions et instructions

    III-A. Introduction aux types de données du langage C

    III-A-1. Les types de base du langage C

    Au niveau du processeur, toutes les données sont représentées sous leur forme binaire et la notion de type n'a pas de sens. Cette notion n'a été introduite que par les langages de haut niveau dans le but de rendre les programmes plus rationnels et structurés. Mais même parmi les langages de haut niveau, on distingue ceux qui sont dits fortement typés (qui offrent une grande variété de types), comme le Pascal par exemple, de ceux qui sont dits faiblement ou moyennement typés (et plus proches de la machine) comme le C par exemple. Le langage C ne dispose que de 4 types de base :

    Type C | Type correspondant
char | caractère (entier de petite taille)
int | entier
float | nombre flottant (réel) en simple précision
double | nombre flottant (réel) en double précision

    Devant char ou int, on peut mettre le modificateur signed ou unsigned selon que l'on veut avoir un entier signé (par défaut) ou non signé. Par exemple :

    char ch;
unsigned char c;
unsigned int n; /* ou tout simplement : unsigned n */

    La plus petite valeur possible que l'on puisse affecter à une variable de type entier non signé est 0 alors que les entiers signés acceptent les valeurs négatives.

    Devant int, on peut mettre également short ou long auxquels cas on obtiendrait un entier court (short int ou tout simplement short) respectivement un entier long (long int ou tout simplement long). Voici des exemples de déclarations valides :

    int n = 10, m = 5;
short a, b, c;
long x, y, z = -1;
unsigned long p = 2;

    long peut être également mis devant double, le type résultant est alors long double (quadruple précision).

    III-A-2. Règle d'écriture des constantes littérales

    Nombres entiers

    Toute constante littérale « pure » de type entier (ex : 1, -3, 60, 40, -20, ...) est considérée par le langage comme étant de type int.

    Pour expliciter qu'une constante littérale de type entier est de type unsigned, il suffit d'ajouter à la constante le suffixe u ou U. Par exemple : 2u, 30u, 40U, 50U, ...

    De même, il suffit d'ajouter le suffixe l ou L pour expliciter qu'une constante littérale de type entier est de type long (on pourra utiliser le suffixe UL par exemple pour unsigned long).

    Une constante littérale de type entier peut également s'écrire en octal (base 8) ou en hexadécimal (base 16). L'écriture en hexa est évidement beaucoup plus utilisée.

    Une constante littérale écrite en octal doit être précédée de 0 (zéro). Par exemple : 012, 020, 030UL, etc.

    Une constante littérale écrite en hexadécimal doit commencer par 0x (zéro x). Par exemple 0x30, 0x41, 0x61, 0xFFL, etc.

    Nombres flottants

    Toute constante littérale « pure » de type flottant (ex : 0.5, -1.2, ...) est considérée comme étant de type double.

    Le suffixe f ou F permet d'expliciter un float. Attention, 1f n'est pas valide car 1 est une constante entière. Par contre 1.0f est tout à fait correcte. Le suffixe l ou L permet d'expliciter un long double.

    Une constante littérale de type flottant est constituée, dans cet ordre :

    • d'un signe (+ ou -)
    • d'une suite de chiffres décimaux : la partie entière
    • d'un point : le séparateur décimal
    • d'une suite de chiffres décimaux : la partie décimale
    • d'une des deux lettres e ou E : symbole de la puissance de 10 (notation scientifique)
    • d'un signe (+ ou -)
    • d'une suite de chiffres décimaux : la puissance de 10

    Par exemple, les constantes littérales suivantes représentent bien des nombres flottants : 1.0, -1.1f, 1.6E-19, 6.02e23L, 0.5 3e8

    Caractères et chaînes de caractères

    Les caractères sont placés entre simple quottes (ex : 'A', 'b', 'c', ...) et les chaînes de caractères entre double quottes (ex : "Bonjour", "Au revoir", ...). Certains caractères sont spéciaux et il faut utiliser la technique dite d'échappement pour les utiliser. En langage C, le caractère d'échappement est le caractère \. Exemples :

    Caractère | Valeur
'\t' | Le caractère 'tabulation'
'\r' | Le caractère 'retour chariot'
'\n' | Le caractère 'fin de ligne'
'\\' | Le caractère 'antislash'

    III-A-3. Spécification de format dans printf

    III-A-3-a. Généralités

    Voici la liste des codes format que nous utiliserons les plus souvent par la suite :

    Code format | Utilisation
c | Afficher un caractère
d | Afficher un int
u | Afficher un unsigned int
x, X | Afficher un entier dans le format hexadécimal
f | Afficher un float ou un double en notation décimale
e | Afficher un float ou un double en notation scientifique avec un petit e
E | Afficher un float ou un double en notation scientifique avec un grand E
g, G | Afficher un float ou un double (utilise le format le plus adapté)
% | Afficher le caractère '%'

    Lorsqu'on affiche une donnée de type flottant dans le format "scientifique", il n'y a qu'un chiffre, ni moins ni plus, devant le point décimal (et ce chiffre est 0 si et seulement si la donnée vaut 0), le nombre de chiffres après ce point est variable et le nombre de chiffres utilisés pour représenter l'exposant dépend de l'implémentation mais il y en a toujours au moins deux.

    D'autre part :

    • h devant d ou u indique que l'argument est un short
    • l devant d, u, x ou X indique que l'argument est de type long
    • L devant f, e, E ou g indique que l'argument est de type long double

    La spécification de format dans printf va cependant beaucoup plus loin qu'une simple spécification de type. En règle générale, une spécification de format a la structure suivante : %[options]specificateur_de_type. Les "options" ont été mis entre crochets car elles sont toujours facultatives. Les options disponibles dépendent du code de conversion (le "spécificateur de type") utilisé. Nous n'allons pas toutes les présenter (la documentation de référence est là pour ça) mais allons quand même voir quelques options que nous aurons assez souvent l'occasion d'utiliser.

    Enfin, pour afficher un caractère, il vaut mieux utiliser putchar (ex : putchar('*')) qui est plus simple plutôt que printf.

    III-A-3-b. Les "options"

    Une spécification de format dans printf a le plus souvent (cas pratique) la structure suivante :

%[[+][-][0]][gabarit][.precision][modificateur_de_type]specificateur_de_type

    Les drapeaux + et 0 n'ont de sens que pour les données à imprimer sous forme numérique.

    Découvrons le rôle des paramètres les plus utiles pour les types les plus utilisés.

    III-A-3-c. Le type entier

    Pour le type entier (code de conversion d, u, x ou X) :

    • h devant le spécificateur de type indique que l'argument attendu est de type short et l indique que l'argument est de type long.
    • La précision indique le nombre maximum de caractères à utiliser pour imprimer la donnée.
    • Le gabarit indique le nombre minimum de caractères à utiliser.
    • Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.
    • Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.
    • Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

    Voici un programme proposant quelques applications :

    #include <stdio.h>

int main()
{
    int n = 1234;

    printf("+------------+\n");
    printf("|%12d|\n", n); /* 12 caracteres minimum, alignement a droite */
    printf("+------------+\n");
    printf("|%-12d|\n", n); /* 12 caracteres minimum, alignement a gauche */
    printf("+------------+\n");
    printf("|%012d|\n", n); /* 12 caracteres minimum, caractere de remplissage : '0' */
    printf("+------------+\n");

    /* Affichage obligatoire du signe */

    printf("|%+12d|\n", n);
    printf("+------------+\n");
    printf("|%+-12d|\n", n);
    printf("+------------+\n");
    printf("|%+012d|\n", n);
    printf("+------------+\n");

    return 0;
}

    Voici la sortie de ce programme :

    +------------+
|        1234|
+------------+
|1234        |
+------------+
|000000001234|
+------------+
|       +1234|
+------------+
|+1234       |
+------------+
|+00000001234|
+------------+
    III-A-3-d. Le type flottant

    Pour le type flottant (code de conversion f, e, E, g ou G) :

    • L devant le spécificateur de type indique que l'argument attendu est de type long double.
    • La précision indique le nombre de caractères à afficher après le point décimal pour les codes f, e et E (la valeur par défaut est 6) et le nombre maximum de chiffres significatifs pour les codes g et G.
    • Le gabarit indique le nombre minimum de caractères à utiliser.
    • Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.
    • Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.
    • Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

    Voici un programme proposant quelques applications :

    #include <stdio.h>

int main()
{
    double x = 12.34;

    /* Affichage de flottant avec 4 chiffres apres le point decimal. */

    printf("%.4f\n", x);
    printf("%.4e\n", x);

    /* Affichage dans le format le plus approprie. */

    printf("%g\n", x);

    return 0;
}

    Voici la sortie de ce programme (en supposant que l'implémentation utilise 3 chiffres pour imprimer l'exposant) :

    12.3400
1.2340e+001
12.34

    III-A-4. Les variables et les constantes

    Si vous avez déjà fait un peu de math dans votre vie, vous devriez connaître ce que c'est qu'une variable. La notion est pratiquement la même en programmation. La déclaration d'une variable en langage C, dans sa forme la plus simple, se fait de la manière suivante :

    Sélectionnez
    <type> variable;

    Par exemple :

    Sélectionnez
    int n;

    Voici d'autres exemples beaucoup plus généralistes :

    Sélectionnez
    int a, b, c;
int i = 0, j, n = 10;
double x, y, z = 0.5;

    Une déclaration de variable de la forme :

    int n = 0;

    est appelée une déclaration avec initialisation. Ce n'est pas la même chose que :

    int n;
n = 0;

    qui est une déclaration suivie d'une instruction d'affectation. Pour vous en convaincre rapidement, une variable constante (ou varaible en lecture seule) est une variable qui ne peut être qu'initialisée puis lue (la valeur d'une constante ne peut pas être modifiée). On obtient une constante en ajoutant le mot-clé const devant une déclaration. Par exemple :

    const int n = 10;

    On ne pourra jamais écrire :

    n = <quoi que ce soit>;

    Le mot-clé const être placé avant ou après le type ou encore avant le nom de la variable. Voici deux exemples :

    const int n = 10;
int const m = 20;

    III-A-5. Définition de nouveaux types

    Le C dispose d'un mécanisme très puissant permettant au programmeur de créer de nouveaux types de données en utilisant le mot clé typedef. Par exemple :

    typedef int ENTIER;

    Définit le type ENTIER comme n'étant autre que le type int. Rien ne nous empêche donc désormais d'écrire par exemple :

    Sélectionnez
    ENTIER a, b;

    Bien que dans ce cas, un simple #define aurait pu suffire, il est toujours recommandé d'utiliser typedef qui est beaucoup plus sûr.

    III-B. Les pointeurs

    III-B-1. Définition

    Comme nous le savons très bien, l'endroit où s'exécute un programme est la mémoire donc toutes les données du programme (les variables, les fonctions, ...) se trouvent en mémoire. Le langage C dispose d'un opérateur - & - permettant de récupérer l'adresse en mémoire d'une variable ou d'une fonction quelconque. Par exemple, si n est une variable, &n désigne l'adresse de n.

    Le C dispose également d'un opérateur - * - permettant d'accéder au contenu de la mémoire dont l'adresse est donnée. Par exemple, supposons qu'on ait :

    int n;

    Alors les instructions suivantes sont strictement identiques.

    n = 10;

    *( &n ) = 10;

    Un pointeur (ou une variable de type pointeur) est une variable destinée à recevoir une adresse. On dit alors qu'elle pointe sur un emplacement mémoire. L'accès au contenu de la mémoire se fait par l'intermédiaire de l'opérateur *.

    Les pointeurs en langage C sont typés et obéissent à l'arithmétique des pointeurs que nous verrons un peu plus loin. Supposons que l'on veuille créer une variable - p - destinée à recevoir l'adresse d'une variable de type int. p s'utilisera alors de la façon suivante :

    p = &n;
...
*p = 5;
...

    *p est un int. Vous suivez ? Alors comment devrait-on déclarer le pointeur ? Si vous avez bien suivi, vous auriez répondu :

    int *p;

    Et de plus, si vous avez très bien suivi, vous auriez certainement ajouté : et cela est strictement équivalent à :

    int * p;

    Car en C, les espaces sont totalement gratuites ! Donc voilà, le type d'un pointeur sur int est int *. Mais cela peut être moins évident dans certaines déclarations. Par exemple, dans :

    /* cas 1 */
int * p1, p2, p3;

    seul p1 est de type int * ! Le reste, p2 et p3, sont de type int. Si vous voulez obtenir 3 pointeurs, vous devrez utilisez la syntaxe suivante :

    /* cas 2 */
int *p1, *p2, *p3;

    Ou avec un typedef :

    typedef int * PINT;
PINT p1, p2, p3;

    Par contre cela n'aurait pas marché si on avait défini PINT à l'aide d'un #define car cela nous amènerait au cas 1.

    III-B-2. Saisir des données tapées au clavier avec la fonction scanf▲

    On va encore faire un petit exercice de logique, plutôt de bon sens.

    Si nous voulons afficher un entier par exemple, que doit-on fournir à la fonction d'affichage (printf par exemple) : la valeur de l'entier que nous voulons afficher ou l'adresse de la variable contenant le nombre que nous voulons afficher ?

    La bonne réponse est : l'entier que nous voulons afficher bien sûr, autrement dit sa valeur. Personne n'a demandé son adresse !

    Maintenant, si nous voulons demander à l'utilisateur (celui qui utilise notre programme) de taper un nombre puis ranger le nombre ainsi tapé dans une variable, que devons-nous fournir à la fonction permettant la saisie : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre que l'utilisateur a entré, ou la valeur que contient actuellement cette variable ?

    La bonne réponse est bien sûr : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre entré

    La fonction scanf, déclarée dans le fichier stdio.h, permet de lire des données formatées sur l'entrée standard, par défaut le clavier.

    Voici un programme qui demande à l'utilisateur d'entrer 2 nombres puis affiche leur somme :

    #include <stdio.h>

int main()
{
    int a, b, c;

    printf("Ce programme calcule la somme de 2 nombres.\n");

    printf("Entrez la valeur de a : ");
    scanf("%d", &a);

    printf("Entrez la valeur de b : ");
    scanf("%d", &b);

    c = a + b;
    printf("%d + %d = %d\n", a, b, c);

    return 0;
}

    Mais faites gaffe avec la fonction scanf ! "%d" par exemple n'a rien à voir avec " %d ". En effet dans ce dernier cas, le format attendu par la fonction est :

    <un espace> <un entier> <puis un autre espace>

    Où espace désigne en fait n'importe quel caractère ou série de caractères blancs (espace, tabulation, etc.). Voici un exemple beaucoup plus convaincant :

    #include <stdio.h>

int main()
{
    int mon_age = 23, votre_age;

    printf("Bonjour, j'ai %d ans. Et vous? ", mon_age);
    scanf("%d ans", &votre_age);
    printf("Ah! vous avez %d ans?\n", votre_age);

    return 0;
}

    L'utilisateur devra alors taper par exemple :

    22 ans

    Peu importe le nombre de caractères blancs entre 22 et ans.

    C'est ce qu'on appelle une saisie formatée. scanf retourne le nombre de conversions de format réussies. La valeur de retour de scanf permet donc déjà de voir si l'utilisateur a bien respecté le format attendu ou non. Dans notre exemple, il n'y a qu'une seule conversion de format à faire (%d). Si l'utilisateur respecte bien le format, scanf retournera 1.

    Les fonctions telles que scanf sont plutôt destinées à être utilisées pour lire des données provenant d'un programme sûr (par l'intermédiaire d'un fichier par exemple), pas celles provenant d'un humain, qui sont sujettes à l'erreur. Les codes format utilisés dans scanf sont à peu près les mêmes que dans printf, sauf pour les flottants notamment.

    Code format | Utilisation
f, e, g | float
lf, le, lg | double
Lf, Le, Lg | long double

    Voici un programme qui permet de calculer le volume d'un cône droit à base circulaire selon la formule : V = 1/3 * (B * h) où B est la surface de base soit pour une base circulaire : B = PI*R2, où R est le rayon de la base.

    #include <stdio.h>

double Volume(double r_base, double hauteur);

int main()
{
    double R, h, V;

    printf("Ce programme calcule le volume d'un cone.\n");

    printf("Entrez le rayon de la base : ");
    scanf("%lf", &R);

    printf("Entrez la hauteur du cone : ");
    scanf("%lf", &h);

    V = Volume(R, h);
    printf("Le volume du cone est : %f", V);

    return 0;
}

double Volume(double r_base, double hauteur)
{
    return (3.14 * r_base * r_base * hauteur) / 3;
}

    III-B-3. Exemple de permutation des contenus de deux variables

    Voici un programme simple qui permute le contenu de deux variables.

    include <stdio.h>

int main()
{
    int a = 10, b = 20, c;

    /* on sauvegarde quelque part le contenu de a */
    c = a;

    /* on met le contenu de b dans a */
    a = b;

    /* puis le contenu de a que nous avons sauvegardé dans c dans b */
    b = c;

    /* affichons maintenant a et b */
    printf("a = %d\nb = %d\n", a, b);

    return 0;
}

    Maintenant, réécrivons le même programme en utilisant une fonction. Cette fonction doit donc pouvoir localiser les variables en mémoire autrement dit nous devons passer à cette fonction les adresses des variables dont on veut permuter le contenu.

    #include <stdio.h>

void permuter(int * addr_a, int * addr_b);

int main()
{
    int a = 10, b = 20;

    permuter(&a, &b);
    printf("a = %d\nb = %d\n", a, b);

    return 0;
}

void permuter(int * addr_a , int * addr_b)
/***************\
* addr_a <-- &a *
* addr_b <-- &b *
\***************/
{
    int c;

    c = *addr_a;
    *addr_a = *addr_b;
    *addr_b = c;
}

    III-C. Les expressions

    III-C-1. Introduction

    En langage C, la notion d'expression est beaucoup plus riche que dans n'importe quel autre langage. De ce fait en donner une définition est longue et nous allons donc plutôt l'introduire (comme nous l'avons d'ailleurs toujours fait) de manière intuitive. Pour cela, analysons l'instruction suivante :

    c = a + b;

    Dans cette instruction (où nous supposons que les 3 variables sont tous de type int), il y a tout d'abord évaluation de a + b, puis affection du résultat dans c. On dit que :

    a + b

    est une expression, plus précisément une expression de type int (car a et b sont de type int) et dont la valeur est la somme de a et b (a + b), et que :

    c = a + b;

    est une instruction (remarquez bien le point-virgule).

    L'expression a + b est composée de deux expressions à savoir a et b. On dit alors que c'est une expression complexe. En langage C, les opérateurs permettent de construire des expressions complexes. Dans notre exemple, l'opérateur d'addition à savoir + nous a permis de construire une expression complexe, a + b, à partir des expressions simples a et b.

    Or en langage C, = est également un opérateur, l'opérateur d'affectation, et permet donc de construire des expressions complexes. Ainsi, dans notre exemple :

    c = a + b

    est une expression (remarquez bien l'absence de point-virgule, sinon ce serait une instruction) et dont la valeur et le type sont celle et celui de l'expression situé à gauche à savoir c.

    III-C-2. lvalue et rvalue

    Une lvalue (de left value) est tout ce qui peut figurer à gauche de l'opérateur d'affectation. Une variable, par exemple, est une lvalue. Si p est un pointeur (disons vers int), p est une lvalue (en effet un pointeur est une variable comme toutes les autres) et *p est également une lvalue.

    Une rvalue (de right value) est tout ce qui peut figurer à droite de l'opérateur d'affectation. Une variable, une constante littérale, l'adresse d'une variable en sont des exemples. Si a est une variable, disons de type int, alors a est une lvalue tandis que a, -a ou a + 1 par exemple sont des rvalues. Cet exemple nous montre bien qu'une variable est à la fois une lvalue et une rvalue.

    Une rvalue possède une valeur mais pas une adresse. Ainsi par exemple : a étant une lvalue, &a a bien un sens (désigne l'adresse mémoire de la variable a). Par contre, une expression insensée telle que &(-a) ou &(a + 1) sera rejetée (avec plaisir) par le compilateur.

    III-C-3. Opérations usuelles

    III-C-3-a. Les opérateurs arithmétiques courants

    Les opérateurs arithmétiques courants +, -, * et / existent en langage C. Toutefois, la division entière est un tout petit peu délicate. En effet, si a et b sont des entiers, a / b vaut le quotient de a et b c'est-à-dire par exemple, 29 / 5 vaut 5. Le reste d'une division entière s'obtient avec l'opérateur modulo %, c'est-à-dire, en reprenant l'exemple précédent, 29 % 5 vaut 4.

    III-C-3-b. Les opérateurs de comparaison

    Les opérateurs de comparaison sont

    Opérateur | Rôle
< | Inférieur à
> | Supérieur à
== | Egal à
<= | Inférieur ou égal à
>= | Supérieur ou égal à
!= | Différent de

    Par exemple, l'expression :

    1 < 1000

    est vraie. Vraie ? Et que vaut « vrai » au juste ? Quel est le type d'une expression « vraie » ? En langage C, la valeur d'une expression « vraie » est non nulle et celle d'une expression « fausse » zéro. Et réciproquement, toute valeur non nulle (24, -107, 2.9, ...) peut être interprétée comme vrai et zéro comme faux. Par exemple :

    int prop;
prop = (1 < 1000); /* alors prop = VRAI */
    III-C-3-c. Les opérateurs logiques

    On peut construire des expressions logiques complexes à l'aide des opérateurs

    Opérateur | Rôle
&& | ET
|| | OU
! | NON

    Par exemple :

    int prop1, prop2, prop_ou, prop_et, prop_vrai;
prop1 = (1 < 1000);
prop2 = (2 == -6);
prop_ou = prop1 || prop2; /* VRAI, car prop1 est VRAI */
prop_et = prop1 && prop2; /* FAUX, car prop2 est FAUX */
prop_vrai = prop1 && !prop_2 /* VRAI car prop1 et !prop2 sont VRAI */

    Les opérateurs logiques jouissent des propriétés suivantes :

    • Dans une opération ET, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est fausse, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours FAUX.

    • Dans une opération OU, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est vrai, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours VRAI.

    III-C-4. L'opérateur virgul

    On peut séparer plusieurs expressions à l'aide de l'opérateur virgule. Le résultat est une expression dont la valeur est celle de l'expression la plus à droite. L'expression est évaluée de gauche à droite. Par exemple, l'expression :

    (a = -5, b = 12, c = a + b) * 2

    vaut 14.

    III-C-5. Taille des données. L'opérateur sizeof

    La taille d'une donnée désigne la taille, en octets, que celle-ci occupe en mémoire. Par extension de cette définition, la taille d'un type de données désigne la taille d'une donnée de ce type. Attention ! octet désigne ici, par abus de langage, la taille d'un élément de mémoire sur la machine cible (la machine abstraite), c'est-à-dire la taille d'une case mémoire (qui vaut 8 bits dans la plupart des architectures actuelles), et non un groupe de 8 bits. En langage C, un octet (une case mémoire) est représenté par un char. La taille d'un char n'est donc pas forcément 8 bits, même si c'est le cas dans de nombreuses architectures, mais dépendante de la machine. La norme requiert toutefois qu'un char doit faire au moins 8 bits et que la macro CHAR_BIT, déclarée dans limits.h, indique la taille exacte d'un char sur la machine cible.

    Le C dispose d'un opérateur, sizeof, permettant de connaître la taille, en octets, d'une donnée ou d'un type de données. La taille d'un char vaut donc évidemment 1 puisqu'un char représente un octet. Par ailleurs, il ne peut y avoir de type dont la taille n'est pas multiple de celle d'un char. Le type de la valeur retournée par l'opérateur sizeof est size_t, déclaré dans stddef.h, qui est inclus par de nombreux fichiers d'en-tête dont stdio.h.

    Comme nous l'avons déjà dit plus haut, la taille des données est dépendante de la machine cible. En langage C, la taille des données n'est donc pas fixée. Néanmoins la norme stipule qu'on doit avoir :

    sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeofd(long)

    Sur un processeur Intel (x86) 32 bits par exemple, un char fait 8 bits, un short 16 bits, et les int et les long 32 bits.

    III-C-6. Les opérateurs d'incrémentation et de décrémentation▲

    Il s'agit des opérateurs ++ (opérateur d'incrémentation) et -- (opérateur de décrémentation). Ils existent sous forme préfixée ou postfixée. L'opérande doit être une lvalue de type numérique ou pointeur. Soient i et j deux variables de type int :

    j = i++;

    est équivalent à :

    j = i;
i = i + 1;

    On voit donc bien que la valeur de i++ est celle de i avant l'évaluation effective de l'expression (post incrémentation).

    Inversement :

    j = ++i;

    est équivalent à :

    i = i + 1;
j = i;

    Où l'on voit bien que la valeur de ++i est celle de i après l'évaluation effective de l'expression (pré incrémentation).

    L'opérateur -- s'utilise de la même manière que ++ mais l'opérande est cette fois-ci décrémentée.

    III-C-7. Expressions conditionnelles

    Une expression conditionnelle est une expression dont la valeur dépend d'une condition. L'expression :

    p ? a : b

    vaut a si p est vrai et b si p est faux.

    III-C-8. Autres opérateurs d'affectation

    Ce sont les opérateurs : +=, -=, *=, /=, ...

    x += a;

    par exemple est équivalent à :

    x = x + a;

    III-C-9. Ordre de priorité des opérateurs

    Les opérateurs sont classés par ordre de priorité. Voici les opérateurs que nous avons étudiés jusqu'ici classés dans cet ordre. Opérateur Associativité

    Opérateur | Associativité
Parenthèses | de gauche à droite
! ++ -- - (signe) sizeof | de gauche à droite
* / % | de gauche à droite
+ - | de gauche à droite
< <= > >= | de gauche à droite
== != | de gauche à droite
& (adresse de) | de gauche à droite
&& | de gauche à droite
|| | de gauche à droite
Opérateurs d'affectation (= += ...) | de droite à gauche
, | de gauche à droite

    Ce n'est pas parce que cet ordre existe qu'il faut le retenir par coeur. Pour du code lisible, il est même conseillé de ne pas trop en tenir compte et d'utiliser des parenthèses dans les situations ambiguës.

    III-D. Considérations liées à la représentation binaire

    III-D-1. Généralités

    L'ordinateur traite les données uniquement sous forme numérique. A l'intérieur de l'ordinateur, ces nombres sont représentés en binaire. La manière de représenter une donnée dans la mémoire de l'ordinateur est appelée codage. Pour bien comprendre le C, vous devez connaître comment les données sont représentées en mémoire, c'est-à-dire sous leur forme binaire. Voir à ce sujet votre cours de numérique.

    III-D-2. Les caractères

    La représentation numérique des caractères définit ce qu'on appelle un jeu de caractères. Par exemple, dans le jeu de caractères ASCII (American Standard Code for Information Interchange) qui est un jeu de caractères qui n'utilise que 7 bits et qui est à la base de nombreux codes populaires de nos jours, le caractère 'A' est représenté par le code 65, le caractère 'a' par 97 et '0' par 48. Hélas, même ASCII n'est pas adopté par le langage C. En effet si le C dépendait d'un jeu de caractères particulier, il ne serait alors pas totalement portable. Le standard définit néanmoins un certain nombre de caractères que tout environnement compatible avec le C doit posséder parmi lesquels les 26 lettres de l'alphabet latin (donc en fait 52 puisqu'on différencie les majuscules et les minuscules), les 10 chiffres décimaux, les caractères # < > ( ) etc. Le programmeur (mais pas le compilateur) n'a pas besoin de connaître comment ces caractères sont représentés dans le jeu de caractères de l'environnement. Le standard ne définit donc pas un jeu de caractères mais seulement un ensemble de caractères que chaque environnement compatible est libre d'implémenter à sa façon (plus éventuellement les caractères spécifiques à cet environnement). La seule contrainte imposée est que leur valeur doit pouvoir tenir dans un char.

    Concernant la technique d'échappement, sachez également qu'on peut insérer du code octal (commençant par 0) ou hexadécimal (commençant par x) après le caractère d'échappement \ pour obtenir un caractère dont le code dans le jeu de caractères est donné. L'hexadécimal est de loin le plus utilisé. Par exemple : '\x30', '\x41', '\x61', ... Et enfin pour les caractères de code 0, 1, ... jusqu'à 7, on peut utiliser les raccourcis '\0', '\1', ... '\7'.

    III-D-3. Dépassement de capacité

    Le dépassement de capacité a lieu lorsqu'on tente d'affecter à une lvalue une valeur plus grande que ce qu'elle peut contenir. Par exemple, en affectant une valeur sur 32 bits à une variable ne pouvant contenir que 16 bits.

    III-E. La conversion de type

    III-E-1. Conversion implicite

    En langage C, des règles de conversion dite implicite s'appliquent aux données qui composent une expression complexe lorsqu'ils ne sont pas de même type (entier avec un flottant, entier court avec entier long, entier signé avec un entier non signé, etc.). Par exemple, dans l'expression :

    'A' + 2

    'A' est de type char et 2 de type int. Dans ce cas, 'A' est tout d'abord converti en int avant que l'expression ne soit évaluée. Le résultat de l'opération est de type int (car un int + un int donne un int). Ici, il vaut 67 (65 + 2). En fait, les char et les short sont toujours systématiquement convertis en int c'est-à-dire que dans l'addition de deux char par exemple, tous deux sont tout d'abord convertis en int avant d'être additionnés, et le résultat est un int (pas un char). Un unsigned char sera converti en unsigned int, et ainsi de suite.

    En règle générale : le type le plus « faible » est convertit dans le type le plus « fort ». Par exemple, les entiers sont plus faibles que les flottants donc 1 mélangé à un flottant par exemple sera tout d'abord converti en 1.0 avant que l'opération n'ait effectivement lieu.

    III-E-2. Conversion explicite (cast)

    Il suffit de préciser le type de destination entre parenthèses devant l'expression à convertir. Par exemple :

    float f;
f = (float)3.1416;

    Dans cet exemple, on a converti explicitement 3.1416, qui est de type double, en float. Lorsqu'on affecte un flottant à un entier, seule la partie entière, si elle peut être représentée, est retenue.

    III-F. Les instructions

    III-F-1. Introduction

    L'instruction est la plus petite unité qui compose une fonction. Le seul type d'instruction que nous avons vu jusqu'ici est l'expression-instruction qui n'est autre qu'une expression suivie d'un point-virgule. Comprendre les notions d'expression et d'instruction est indispensable pour bien comprendre et maîtriser la syntaxe du langage C. Nous allons maintenant généraliser la notion d'instruction en langage C.

    III-F-2. Bloc d'instructions

    Un bloc, abusivement appelé bloc d'instructions, ou plus précisément une instruction-bloc consiste en un groupe de déclarations et d'instructions (qui peuvent être également des blocs). Un bloc est délimité par des accolades. Une fonction est toujours constituée d'une instruction-bloc.

    Une variable déclarée à l'intérieur d'un bloc est locale à (c'est-à-dire n'est connue qu'à l'intérieur de) ce bloc et masque toute variable de même nom déclarée à l'extérieur de celui-ci (une variable globale est une variable déclarée à l'extérieur de toute fonction et est connue partout dans le fichier source). D'autre part, une déclaration doit toujours se faire en début de bloc. Les arguments effectifs d'une fonction sont également des variables locales à cette fonction.

    Voici un exemple de programme (présentant peu d'intérêt je le conçois) comportant plusieurs blocs :

    #include <stdio.h>

int main()
{
    printf("Les instructions en langage C\n");
    printf("-----------------------------\n\n");

    {
        /* Un bloc d'instructions */
        printf("**********\n");
        printf("* Bloc 1 *\n");
        printf("**********\n");
    }

    putchar('\n');

    {
        /* Un autre bloc */
        printf("**********\n");
        printf("* Bloc 2 *\n");
        printf("**********\n");

        {
            /* Encore un autre bloc */
            printf("**********\n");
            printf("* Bloc 3 *\n");
            printf("**********\n");
        }
    }

    return 0;
}

    Retenez donc bien ceci : un bloc constitue bien une et une seule instruction. Bien que cette instruction soit en fait un bloc d'instructions (et de déclarations), il est conseillé de la voir comme une et une seule instruction plutôt que comme un bloc d'instructions. Une instruction-bloc ne se termine pas par un point-virgule. Conclusion : Il est faux de dire qu'une instruction doit toujours se terminer par un point-virgule. Le point-virgule marque tout simplement la fin de certaines instructions, dont les expressions-instructions.

    III-F-3. L'instruction if

    Permet d'effectuer des choix conditionnels. La syntaxe de l'instruction est la suivante :

    if ( <expression> )
    <une et une seule instruction>
else
    <une et une seule instruction>

    Une instruction if peut ne pas comporter de else. Lorsqu'on a plusieurs instructions if imbriquées, un else se rapporte toujours au dernier if suivi d'une et une seule instruction. Par exemple : écrivons un programme qui compare deux nombres.

    #include <stdio.h>

int main()
{
    int a, b;

    printf("Ce programme compare deux nombres.\n");

    printf("Entrez la valeur de a : ");
    scanf("%d", &a);

    printf("Entrez la valeur de b : ");
    scanf("%d", &b);

    if (a < b)
        printf("a est plus petit que b.\n");
    else
        if (a > b)
            printf("a est plus grand que b.\n");
        else
            printf("a est egal a b.\n");

    return 0;
}

    III-F-4. L'instruction do

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    do
    <une et une seule instruction>
while ( <expression> );

    L'instruction do permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait après chaque exécution de l'instruction. Voici un programme qui affiche 10 fois Bonjour.

    #include <stdio.h>

int main()
{
    int nb_lignes_affichees = 0;

    do
        {
            printf("Bonjour.\n");
            nb_lignes_affichees++;
        }
    while (nb_lignes_affichees < 10);

    return 0;
}

    III-F-5. L'instruction while

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    while ( <expression> )
    <une et une seule instruction>

    L'instruction while permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait avant chaque exécution de l'instruction. Donc si la condition (<expression>) est fausse dès le départ, la boucle ne sera pas exécutée.

    III-F-6. L'instruction for

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    for ( <init> ; <condition> ; <step>)
    <instruction>

    Elle est pratiquement identique à :

    <init>;
while ( <condition> )
{
    <instruction>
    <step>
}

    Par exemple, écrivons un programme qui affiche la table de multiplication par 5.

    #include <stdio.h>

int main()
{
    int n;

    for(n = 0; n <= 10; n++)
        printf("5 x %2d %2d\n", n, 5 * n);

    return 0;
}

    III-F-7. Les instructions switch et case

    Ces instructions permettent d'éviter des instructions if trop imbriquées comme illustré par l'exemple suivant :

    #include <stdio.h>

int main()
{
    int n;

    printf("Entrez un nombre entier : ");
    scanf("%d", &n);

    switch(n)
    {
    case 0:
        printf("Cas de 0.\n");
        break;

    case 1:
        printf("Cas de 1.\n");
        break;

    case 2: case 3:
        printf("Cas de 2 ou 3.\n");
        break;

    case 4:
        printf("Cas de 4.\n");
        break;

    default:
        printf("Cas inconnu.\n");
    }

    return 0;
}

    III-F-8. L'instruction break

    Permet de sortir immédiatement d'une boucle ou d'un switch. La syntaxe de cette instruction est :

    break;

    III-F-9. L'instruction continue

    Dans une boucle, permet de passer immédiatement à l'itération suivante. Par exemple, modifions le programme table de multiplication de telle sorte qu'on affiche rien pour n = 4 ou n = 6.

    #include <stdio.h>

int main()
{
    int n;

    for(n = 0; n <= 10; n++)
    {
        if ((n == 4) || (n == 6))
            continue;

        printf("5 x %2d %2d\n", n, 5 * n);
    }

    return 0;
}

    III-F-10. L'instruction return

    Permet de terminer une fonction. La syntaxe de cette instruction est la suivante :

    return <expression>; /* termine la fonction et retourne <expression> */

    ou :

    return; /* termine la fonction sans spécifier de valeur de retour */

    III-F-11. L'instruction vide

    Une instruction peut être vide. Par exemple :

    /* Voici une instruction vide * / ;
/* Voici une autre instruction vide */ ;
{ /* Encore une instruction vide */ }

    III-G. Exercices

    III-G-1. Valeur absolue

    Ecrire un programme qui demande à l'utilisateur d'entrer un nombre et qui affiche ensuite la "valeur absolue" de ce nombre. Il y a plusieurs façons de définir la valeur absolue d'un nombre x mais nous utiliserons la suivante : c'est x si x est supérieur ou égal à 0 et -x si x est inférieur à 0. La valeur absolue d'un nombre est donc un nombre toujours positif ou nul. Voici un exemple d'exécution :

    Ce programme permet de determiner la valeur absolue d'un nombre.
Entrez un nombre : -6
La valeur absolue de -6 est 6.
Merci d'avoir utilise ce programme. A bientot !

    III-G-2. Moyenne

    Ecrire un programme qui demande à l'utilisateur d'entrer une note. La note doit être comprise entre 0 et 20 sinon le programme affichera "Cette note n'est pas valide" puis se terminera. Après que l'utilisateur ait entré une note valide, si la note est supérieure ou égale à 10, le programme affichera "Vous avez eu la moyenne." sinon, il affichera "Vous n'avez pas eu la moyenne.". Voici quelques exemples d'exécution :

    Ce programme permet de determiner si vous avez eu la moyenne ou non.
Entrez votre note (0 a 20) : 12
Vous avez eu la moyenne.
Merci d'avoir utilise ce programme. A bientot !

    Ce programme permet de determiner si vous avez eu la moyenne ou non.
Entrez votre note (0 a 20) : 22
Cette note n'est pas valide.
Merci d'avoir utilise ce programme. A bientot !

    III-G-3. L'heure dans une minute

    Ecrire un programme qui demande à l'utilisateur d'entrer l'heure et la minute actuelles et qui affiche ensuite l'heure et la minute qu'il fera une minute après. Voici quelques exemples d'exécution :

    Ce programme permet de determiner l'heure qu'il sera une minute plus tard.
Entrez l'heure actuelle : 14
Entrez la minute actuelle : 38
Dans une minute il sera : 14:39.
Merci d'avoir utilise ce programme. A bientot !

    Ce programme permet de determiner l'heure qu'il sera une minute plus tard.
Entrez l'heure actuelle : 23
Entrez la minute actuelle : 59
Dans une minute il sera : 00:00.
Merci d'avoir utilise ce programme. A bientot !

    Pour simplifier l'exercice, on supposera que les valeurs entrées par l'utilisateur seront toujours correctes.

    III-G-4. Rectangle

    a. Ecrire une fonction affiche_car qui reçoit en arguments un caractère c et un entier n - void affiche_car(char c, int n) - et qui imprime ce caractère n fois. Utilisez cette fonction pour écrire un programme qui demande à l'utilisateur d'entrer un nombre n puis qui affiche une ligne de longueur n en utilisant le caractère '*', c'est-à-dire une ligne composée de n '*'. n doit être compris entre 0 et 20. Voici un exemple d'exécution :

    Ce programme dessine une ligne.
Entrez la longueur de la ligne (0 a 20) : 10
**********
Merci d'avoir utilise ce programme. A bientot !

    b. En utilisant la fonction affiche_car, écrire un programme qui demande à l'utilisateur d'entrer deux nombres L et h puis qui dessine un rectangle de longueur L et de hauteur h à l'aide du caractère '*'. L et h doivent être compris entre 0 et 20. Voici un exemple d'exécution :

    Ce programme dessine un rectangle plein.
Entrez la longueur du rectangle (0 a 20) : 10
Entrez la hauteur du rectangle (0 a 20) : 4
**********
**********
**********
**********
Merci d'avoir utilise ce programme. A bientot !

    c. Reprendre l'exercice b en dessinant cette fois ci un rectanle creux. Voici un exemple d'exécution :

    Ce programme dessine un rectangle creux.
Entrez la longueur du rectangle (0 a 20) : 10
Entrez la hauteur du rectangle (0 a 20) : 4
**********
*        *
*        *
**********
Merci d'avoir utilise ce programme. A bientot !

    III-G-5. Triangle isocèle

    a. Ecrire un programme qui demande à l'utilisateur d'entrer un nombre h puis qui dessine un triangle isocèle de hauteur h et de surface minimale à l'aide du caractère '*'. h doit être compris entre 0 et 20. b. Afficher après le dessin le nombre d'étoiles affichées. Voici un exemple d'exécution :

    Ce programme dessine un triangle.
Entrez la hauteur du triangle (0 a 20) : 4
   *
  ***
 *****
*******
Il y a 16 etoile(s) affichee(s).
Merci d'avoir utilise ce programme. A bientot !

    III-G-6. Somme

    Ecrire un programme qui demande à l'utilisateur d'entrer au plus 10 nombres puis qui affiche la somme des nombres entrés. L'utilisateur peut à tout moment terminer sa liste en entrant 0. Voici quelques exemples d'utilisation :

    Ce programme permet de calculer une somme.
Entrez la liste des nombres a additionner (terminez en entrant 0).
1
3
5
7
0
Le total est : 16.
Merci d'avoir utilise ce programme. A bientot !

    Ce programme permet de calculer une somme.
Entrez la liste des nombres a additionner (terminez en entrant 0).
1
2
3
4
5
6
7
8
9
10
Le total est : 55.
Merci d'avoir utilise ce programme. A bientot !


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • IV. Tableaux, pointeurs et chaînes de caractères
    • IV-A. Les tableaux
      • IV-A-1. Définition
      • IV-A-2. Initialisation
      • IV-A-3. Création d'un type « tableau »
      • IV-A-4. Les tableaux à plusieurs dimensions
      • IV-A-5. Calculer le nombre d'éléments d'un tableau
    • IV-B. Les pointeurs
      • IV-B-1. Les tableaux et les pointeurs
      • IV-B-2. L'arithmétique des pointeurs
      • IV-B-3. Pointeurs constants et pointeurs sur constante
      • IV-B-4. Pointeurs génériques
      • IV-B-5. Exemple avec un tableau à plusieurs dimensions
      • IV-B-6. Passage d'un tableau en argument d'une fonction
    • IV-C. Les chaînes de caractères
      • IV-C-1. Chaîne de caractères
      • IV-C-2. Longueur d'une chaîne
      • IV-C-3. Représentation des chaînes de caractères en langage C
      • IV-C-4. Les fonctions de manipulation de chaîne
        • strcpy, strncpy
        • strcat, strncat
        • strlen
        • strcmp, strncmp
      • IV-C-5. Fusion de chaînes littérales
    • IV-D. Exercices
      • IV-D-1. Recherche dans un tableau
      • IV-D-2. Calcul de la moyenne
      • IV-D-3. Manipulation de chaînes

IV. Tableaux, pointeurs et chaînes de caractères

IV-A. Les tableaux

IV-A-1. Définition

Un tableau est un regroupement d'une ou plusieurs données de même type contigus en mémoire. L'accès à un élément du tableau se fait par un système d'indice, l'indice du premier élément étant 0. Par exemple :

int t[10];

déclare un tableau de 10 éléments (de type int) dont le nom est t. Les éléments du tableau vont donc de t[0], t[1], t[2] ... à t[9]. t est une variable de type tableau, plus précisément (dans notre cas), une variable de type tableau de 10 int (int [10]). Les éléments du tableau sont des int. Ils peuvent être utilisés comme n'importe quelle variable de type int.

IV-A-2. Initialisation

On peut initialiser un tableau à l'aide des accolades. Par exemple :

int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

Bien évidemment, on n'est pas obligé d'initialiser tous les éléments, on aurait donc pu par exemple nous arrêter après le 5ème élément, et dans ce cas les autres éléments du tableau seront automatiquement initialisés à 0. Attention ! une variable locale non initialisée contient « n'importe quoi », pas 0 !

Lorsqu'on déclare un tableau avec initialisation, on peut ne pas spécifier le nombre d'éléments car le compilateur le calculera automatiquement. Ainsi, la déclaration :

int t[] = {0, 10, 20, 30};

est strictement identique à :

int t[4] = {0, 10, 20, 30};

IV-A-3. Création d'un type « tableau »

Tout d'abord, étudions un peu la logique du mot-clé typedef. Pour cela, supposons que l'on veuille pouvoir utiliser indifféremment les formules suivantes pour déclarer un entier :

int x;

et

ENTIER x;

Dans la première forme, on remplace x par ENTIER et on la fait précéder du mot-clé typedef, ce qui nous donne :

typedef int ENTIER;

Concrètement, cela signifie : un ENTIER est tout ce qui suit le mot int dans une déclaration. On peut également formuler une phrase similaire (genre : un TABLEAU est tout ce qui est précédé du mot int et suivi de [10] dans sa déclaration) pour définir un type TABLEAU mais il y a plus malin :

On remplace t par TABLEAU dans sa déclaration puis on ajoute devant le mot-clé typedef, exactement comme ce qu'on a fait avec x et ENTIER. Ainsi, le type tableau de 10 int se définit de la manière suivante :

typedef int TABLEAU[10];

Désormais :

int t[10];

et :

TABLEAU t;

sont strictement équivalents.

IV-A-4. Les tableaux à plusieurs dimensions

On peut également créer un tableau à plusieurs dimensions. Par exemple :

int t[10][3];

Un tableau à plusieurs dimensions n'est en fait rien d'autre qu'un tableau (tableau à une dimension) dont les éléments sont des tableaux. Comme dans le cas des tableaux à une dimension, le type des éléments du tableau doit être parfaitement connu. Ainsi dans notre exemple, t est un tableau de 10 tableaux de 3 int, ou pour vous aider à y voir plus clair :

typedef int TRIPLET[3];
TRIPLET t[10];

Les éléments de t vont de t[0] à t[9], chacun étant un tableau de 3 int.

On peut bien entendu créer des tableaux à 3 dimensions, 4, 5, 6, ...

On peut également initialiser un tableau à plusieurs dimensions. Par exemple :

int t[3][4] = { {0, 1, 2, 3},
                {4, 5, 6, 7},
                {8, 9, 10, 11} };

Qu'on aurait également pu tout simplement écrire :

int t[][4] = { {0, 1, 2, 3},
               {4, 5, 6, 7},
               {8, 9, 10, 11} };

IV-A-5. Calculer le nombre d'éléments d'un tableau

La taille d'un tableau est évidemment le nombre d'éléments du tableau multiplié par la taille de chaque élément. Ainsi, le nombre d'éléments dans un tableau est égal à sa taille divisée par la taille d'un élément. On utilise alors généralement la formule sizeof(t) / sizeof(t[0]) pour connaître le nombre d'éléments d'un tableau t. La macro définie ci-dessous permet de calculer la taille d'un tableau :

#define COUNT(t) (sizeof(t) / sizeof(t[0]))

IV-B. Les pointeurs

IV-B-1. Les tableaux et les pointeurs

Pour nous fixer les idées, considérons le tableau t suivant :

char t[10];

Mais les règles que nous allons établir ici s'appliquent à n'importe quel type de tableau, y compris les tableaux à plusieurs dimensions.

Définissons ensuite le type TABLEAU par :

typedef char TABLEAU[10];

Mais avant d'aller plus loin, j'aimerais déjà préciser que les tableaux et les pointeurs n'ont rien de commun, à part peut-être le fait qu'on peut pointer sur n'importe quel élément d'un tableau (ou sur un tableau ...), tout comme on peut pointer sur n'importe quoi. Mais il y a quand même une chose qui lie les deux notions, c'est que : lorsqu'elle n'est pas utilisée en unique argument de sizeof ou en unique argument de l'opérateur & ("adresse de"), une expression de type tableau est toujours convertie par le compilateur en l'adresse de son premier élément. Cela signifie, dans ces conditions, que si t est un tableau de disons 10 éléments, l'écriture t est strictement équivalente à &t[0], donc t + 1 (qui est strictement équivalent à &(t[0]) + 1) est strictement équivalent à &t[1] et ainsi de suite. Ainsi :

t[5] = '*';

est strictement équivalent à :

*(t + 5) = '*';

Et ainsi de suite.

Dans la pratique, on utilise un pointeur sur un élément du tableau, généralement le premier. Cela permet d'accéder à n'importe quel élément du tableau par simple calcul d'adresse. En effet, comme nous l'avons dit plus haut : t + 1 est équivalent à &(t[1]), t + 2 à &(t[2]), etc.

Voici un exemple qui montre une manière de parcourir un tableau :

#include <stdio.h>

#define COUNT(t) (sizeof(t) / sizeof(t[0]))

void Affiche(int * p, size_t nbElements);

int main()
{
    int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

    Affiche(t, COUNT(t));

    return 0;
}

void Affiche(int * p, size_t nbElements)
{
    size_t i;

    for(i = 0; i < nbElements; i++)
        printf("%d\n", p[i]);
}

IV-B-2. L'arithmétique des pointeurs

L'arithmétique des pointeurs est née des faits que nous avons établis précédemment. En effet si p pointe sur un élément d'un tableau, p + 1 doit pointer sur l'élément suivant. Donc si la taille de chaque élément du tableau est par exemple de 4, p + 1 déplace le pointeur de 4 octets (où se trouve l'élément suivant) et non de un.

De même, puisque l'on devrait avoir (p + 1) - p = 1 et non 4, la différence entre deux adresses donne le nombre d'éléments entre ces adresses et non le nombre d'octets entre ces adresses. Le type d'une telle expression est ptrdiff_t, qui est défini dans le fichier stddef.h.

Et enfin, l'écriture p[i] est strictement équivalente à *(p + i).

Cela montre à quel point le typage des pointeurs est important. Cependant, il existe des pointeurs dits génériques capables de pointer sur n'importe quoi. Ainsi, la conversion d'un pointeur générique en un pointeur d'un autre type par exemple ne requiert aucun cast et vice versa.

IV-B-3. Pointeurs constants et pointeurs sur constante

L'utilisation du mot-clé const avec les pointeurs est au début un peu délicate. En effet, en C on a ce qu'on appelle des pointeurs constants et des pointeurs sur constante (et donc aussi des pointeurs contants sur constante ...). Il faut bien savoir les différencier.

  • Pointeur constant : Le pointeur, qui est une variable comme toute les autres, est déclaré const.
  • Pointeur sur constante : C'est l'objet pointé qui est constant.

Dans le deuxième cas, l'objet pointé n'est pas nécessairement une constante. Seulement, on ne pourra pas le modifier via le pointeur (qui considère que l'objet est une constante). De même, on peut pointer sur une mémoire en lecture seule avec n'importe quel pointeur (et non nécessairement un pointeur sur constante) mais cela ne signifie pas que la mémoire devient désormais accessible en écriture (puisque la mémoire pointée est en lecture seule. Ce n'est pas en pointant là-dessus avec quoi que ce soit qu'on pourra changer cela.). Si le pointeur n'est pas un pointeur sur contante alors qu'il pointe sur une constante, le compilateur acceptera évidemment de modifier le contenu de la mémoire pointée (puisque l'écriture est syntaxiquement correcte), mais le problème se manifestera à l'exécution, lorsque le programme tentera de modifier le contenu de la zone en lecture seule.

Comme int * est le type d'un pointeur sur int, un pointeur constant sur un int est de type

int * const

Si on avait placé le mot-clé const avant int *, on obtiendrait :

const int *

qui correspond plutôt, contrairement à ce qu'on attendait, au type pointeur sur const int (pointeur sur constante) ! Voici deux illustrations de l'utilisation de const avec les pointeurs :

int n, m;
int * const p = &n;
*p = 10;
/* p = &m : INTERDIT ! p est une constante ! */

int n, m;
int const * p = &n; /* Ou const int * p = &n; */
p = &m;
/* *p = 10 : INTERDIT ! *p est une constante ! */

IV-B-4. Pointeurs génériques

Le type des pointeurs génériques est void *. Comme ces pointeurs sont génériques, la taille des données pointées est inconnue et l'arithmétique des pointeurs ne s'applique donc pas à eux. De même, puisque la taille des données pointées est inconnue, l'opérateur d'indirection * ne peut être utilisé avec ces pointeurs, un cast est alors obligatoire. Par exemple :

int n;
void * p;

p = &n;
*((int *)p) = 10; /* p etant desormais vu comme un int *, on peut alors lui appliquer l'operateur *. */

Etant donné que la taille de toute donnée est multiple de celle d'un char, le type char * peut être également utilisé en tant que pointeur universel. En effet, une variable de type char * est un pointeur sur octet autrement dit peut pointer n'importe quoi. Cela s'avère pratique des fois (lorsqu'on veut lire le contenu d'une mémoire octet par octet par exemple) mais dans la plupart des cas, il vaut mieux toujours utiliser les pointeurs génériques. Par exemple, la conversion d'une adresse de type différent en char * et vice versa nécessite toujours un cast, ce qui n'est pas le cas avec les pointeurs génériques.

Dans printf, le spécificateur de format %p permet d'imprimer une adresse (void *) dans le format utilisé par le système.

Et pour terminer, il existe une macro à savoir NULL, définie dans stddef.h, permettant d'indiquer q'un pointeur ne pointe nulle part. Son intérêt est donc de permettre de tester la validité d'un pointeur et il est conseillé de toujours initialiser un pointeur à NULL.

IV-B-5. Exemple avec un tableau à plusieurs dimensions

Soit :

int t[10][3];

Définissons le type TRIPLET par :

typedef int TRIPLET[3];

De façon à avoir :

TRIPLET t[10];

En dehors du cas sizeof et opérateur & ("adresse de"), t représente l'adresse de t[0] (qui est un TRIPLET) donc l'adresse d'un TRIPLET. En faisant t + 1, on se déplace donc d'un TRIPLET soit de 3 int.

D'autre part, t peut être vu comme un tableau de 30 int (3 * 10 = 30) puisque les éléments d'un tableau sont toujours contigus en mémoire. On peut donc accéder à n'importe quel élément de t à l'aide d'un pointeur sur int.

Soit p un pointeur sur int et faisons :

p = (int *)t;

On a alors, numériquement, les équivalences suivantes :

t | p
t + 1 | p + 3
t + 2 | p + 6
... |
t + 9 | p + 27

Prenons alors à présent, le 3ème TRIPLET de t soit t[2].

Puisque le premier élément de t[2] se trouve à l'adresse t + 2 soit p + 6, deuxième se trouve en p + 6 +1 et le troisième et dernier en p + 6 + 2. A près cet entier, on se retrouve au premier élément de t[3], en p + 9.

En conclusion, pour un tableau déclaré :

<type> t[N][M];

on a la formule :

t[i][j] = *(p + N*i + j) /* ou encore p[N*i + j] */

Où évidemment : p = (int *)t.

Et on peut bien sur étendre cette formule pour n'importe quelle dimension.

IV-B-6. Passage d'un tableau en argument d'une fonction

Nous avons déjà vu que le passage d'un tableau en argument d'une fonction se fait tout simplement en passant l'adresse de son premier élément. Sachez également que, en argument d'une fonction,

<type> <identificateur>[]

est strictement équivalent à :

<type> * <identificateur>

Alors que dans une « simple » déclaration, la première déclare un tableau (qui doit être impérativement initialisé) et la deuxième un pointeur. On peut également préciser le nombre d'éléments du tableau mais cette information sera complètement ignorée par le compilateur. Elle n'a donc d'intérêt que pour la documentation.

IV-C. Les chaînes de caractères

IV-C-1. Chaîne de caractères

Par définition, une chaîne de caractères, ou tout simplement : chaîne, est une suite finie de caractères. Par exemple, "Bonjour", "3000", "Salut !", "EN 4", ... sont des chaînes de caractères. En langage C, une chaîne de caractères littérale s'écrit entre double quottes, exactement comme dans les exemples donnés ci-dessus.

IV-C-2. Longueur d'une chaîne

La longueur d'une chaîne est le nombre de caractères qu'elle comporte. Par exemple, la chaîne "Bonjour" comporte 7 caractères ('B', 'o', 'n', 'j', 'o', 'u' et 'r'). Sa longueur est donc 7. En langage C, la fonction strlen, déclarée dans le fichier string.h, permet d'obtenir la longueur d'une chaîne passée en argument. Ainsi, strlen("Bonjour") vaut 7.

IV-C-3. Représentation des chaînes de caractères en langage C

Comme nous l'avons déjà mentionné plus haut, les chaîne de caractères littérales s'écrivent en langage C entre double quottes. En fait, le langage C ne dispose pas vraiment de type chaîne de caractères. Une chaîne est tout simplement représentée à l'aide d'un tableau de caractères.

Cependant, les fonctions manipulant des chaînes doivent être capables de détecter la fin d'une chaîne donnée. Autrement dit, toute chaîne de caractères doit se terminer par un caractère indiquant la fin de la chaîne. Ce caractère est le caractère '\0' et est appelé le caractère nul ou encore caractère de fin de chaîne. Son code ASCII est 0. Ainsi la chaîne "Bonjour" est en fait un tableau de caractères dont les éléments sont 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0', autrement dit un tableau de 8 caractères et on a donc "Bonjour"[0] = 'B', "Bonjour"[1] = 'o', "Bonjour"[2] = 'n', ... "Bonjour"[7] = '\0'. Toutefois, la mémoire utilisée pour stocker la chaîne peut être accessible qu'en lecture, le il n'est donc pas portable de tenter de la modifier.

Les fonctions de manipulation de chaîne de la bibliothèque standard du langage C sont principalement déclarées dans le fichier string.h. Voici un exemple d'utilisation d'une de ces fonctions.

#include <stdio.h>
#include <string.h>

int main()
{
    char t[50];

    strcpy(t, "Hello, world!");
    printf("%s\n", t);

    return 0;
}

Dans cet exemple, la chaîne t ne peut contenir tout au plus que 50 caractères, caractère de fin de chaîne inclus. Autrement dit t ne peut que contenir 49 caractères « normaux » car il faut toujours réserver une place pour le caractère de fin de chaîne : '\0'. On peut aussi bien sûr initialiser une chaîne au moment de sa déclaration, par exemple :

char s[50] = "Bonjour";

Qui est strictement équivalente à :

char s[50] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0'};

Puisque, vu d'un pointeur, la valeur d'une expression littérale de type chaîne n'est autre que l'adresse de son premier élément, on peut utiliser un simple pointeur pour manipuler une chaîne. Par exemple :

char * p = "Bonjour";

Dans ce cas, p pointe sur le premier élément de la chaîne "Bonjour". Or, comme nous l'avons déjà dit plus haut, la mémoire allouée pour la chaîne "Bonjour" est en lecture seule donc on ne peut pas écrire par exemple :

p[2] = '*'; /* Interdit */

Avec un tableau, ce n'est pas l'adresse en mémoire de la chaîne qui est stockée, mais les caractères de la chaîne, copiés caractère par caractère. La mémoire utilisée par le tableau étant indépendante de celle utilisée par la chaîne source, on peut faire ce qu'on veut de notre tableau. La fonction strcpy permet de copier une chaîne vers un autre emplacement mémoire.

Le paragraphe suivant discute des fonctions de manipulation de chaînes en langage C.

IV-C-4. Les fonctions de manipulation de chaîne

strcpy, strncpy
#include <stdio.h>
#include <string.h>

int main()
{
    char t1[50], t2[50];

    strcpy(t1, "Hello, world!");
    strcpy(t2, "*************");
    strncpy(t1, t2, 3);
    printf("%s\n", t1);

    return 0;
}

Attention ! Si t1 n'est pas assez grand pour pouvoir contenir la chaîne à copier, vous aurez un débordement de tampon (buffer overflow). Un tampon (ou buffer) est tout simplement une zone de la mémoire utilisée par un programme pour stocker temporairement des données. Par exemple, t1 est un buffer de 50 octets. Il est donc de la responsabilité du programmeur de ne pas lui passer n'importe quoi ! En effet en C, le compilateur suppose que le programmeur sait ce qu'il fait !

La fonction strncpy s'utilise de la même manière que strcpy. Le troisième argument indique le nombre de caractères à copier. Aucun caractère de fin de chaîne n'est automatiquement ajouté.

strcat, strncat
#include <stdio.h>
#include <string.h>

int main()
{
    char t[50];

    strcpy(t, "Hello, world");
    strcat(t, " from");
    strcat(t, " strcpy");
    strcat(t, " and strcat");
    printf("%s\n", t);

    return 0;
}
strlen

Retourne le nombre de caractères d'une chaîne.

strcmp, strncmp

On n'utilise pas l'opérateur == pour comparer des chaînes car ce n'est pas les adresses qu'on veut comparer mais le contenu mémoire. La fonction strcmp compare deux chaînes de caractères et retourne :

  • zéro si les chaînes sont identiques
  • un nombre négatif si la première est "inférieure" (du point de vue lexicographique) à la seconde
  • et un nombre positif si la première est "supérieure" (du même point de vue ...) à la seconde

Ainsi, à titre d'exemple, dans l'expression

strcmp("clandestin", "clavier")

La fonction retourne un nombre négatif car, 'n' étant plus petit que 'v' (dans le jeu de caractères ASCII, ça n'a rien à voir avec le langage C), "clandestin" est plus petit que "clavier".

IV-C-5. Fusion de chaînes littérales

Le C permet de fusionner deux chaînes littérales en les plaçant simplement côte à côte. Cela se révèle particulièrement utile lorsqu'on doit composer une chaîne et que cette dernière soit trop longue a écrire sur une seule ligne. Par exemple :

#include <stdio.h>

int main()
{
    printf( "Voici une chaine de caracteres particulierement longue, tres longue, "
            "tellement longue (oui, longue !) qu'il a fallu la decouper en deux !" );
    return 0;
}

IV-D. Exercices

IV-D-1. Recherche dans un tableau

Nous nous proposons d'écrire un programme qui permet de trouver toutes les occurrences d'un nombre entré par l'utilisateur parmi un un ensemble de nombres précédemment entrés. L'utilisateur entrera dix nombres (que vous placerez dans un tableau), ensuite il entrera le nombre à rechercher et le programme devra afficher toutes les occurrences de ce nombre s'il y en a ainsi que le nombre d'occurrences trouvées. Voici un exemple d'exécution :

Ce programme permet de trouver toutes les occurrences d'un nombre.
Entrez 10 nombres :
t[0] : 2
t[1] : -1
t[2] : 3
t[3] : 28
t[4] : 3
t[5] : -8
t[6] : 9
t[7] : 40
t[8] : -1
t[9] : 8
Entrez le nombre a rechercher : 3
t[2]
t[4]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

IV-D-2. Calcul de la moyenne

Nous nous proposons d'écrire un programme permettant de calculer la moyenne d'un élève. L'utilisateur entrera 5 "notes". Une note est constituée d'une note sur 20 (de type entier) et d'un coefficient compris entre 1 et 5 (entier également). La note définitive pour une matière est la note sur 20 multipliée par le coefficient. La moyenne de l'étudiant est égale au total des notes définitives divisé par la somme des coefficients.

On créera un tableau t de 5 tableaux de 3 entiers, c'est-à-dire int t[5][3]. Les 5 éléments de t sont destinés à accueillir les 5 notes. Pour chaque note t[i], t[i][0] contiendra la note sur 20, t[i][1] le coefficient et t[i][2] la note définitive. Le programme demandera à l'utilisateur d'entrer les notes puis affichera la moyenne de l'élève avec 2 chiffres après la virgule et les calculs intermédiaires. Voici un exemple d'exécution :

Ce programme permet de calculer votre moyenne scolaire.
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

IV-D-3. Manipulation de chaînes

Ecrire les fonctions de manipulation de chaînes suivantes :

  • str_cpy, de rôle et d'utilisation semblables à la fonction strcpy de la bibliothèque standard
  • str_len, de rôle et d'utilisation semblalbles à la fonction strlen de la bibliothèque standard
  • str_equals, qui reçoit en arguments de chaînes de caractères et qui retourne VRAI si elles sont égales et FAUX dans le cas contraire


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • V. Les entrées/sorties en langage C
    • V-A. Introduction
    • V-B. Les fichiers
    • V-C. Les entrée et sortie standards
    • V-D. Exemple : lire un caractère, puis l'afficher
    • V-E. Saisir une chaîne de caractères
    • V-F. Lire une ligne avec fgets
    • V-G. Mécanisme des entrées/sorties en langage C
      • V-G-1. Le tamponnage
        • V-G-1-a. Les tampons d'entrée/sortie
        • V-G-1-b. Les modes de tamponnage
      • V-G-2. Lire de manière sûre des données sur l'entrée standard

V. Les entrées/sorties en langage C

V-A. Introduction

Les entrées/sorties (E/S) ne font pas vraiment partie du langage C car ces opérations sont dépendantes du système. Cela signifie que pour réaliser des opérations d'entrée/sortie en C, il faut en principe passer par les fonctionnalités offertes par le système. Néanmoins sa bibliothèque standard est fournie avec des fonctions permettant d'effectuer de telles opérations afin de faciliter l'écriture de code portable. Les fonctions et types de données liées aux entrées/sorties sont principalement déclarés dans le fichier stdio.h (standard input/output).

V-B. Les fichiers

Les entrées/sorties en langage C se font par l'intermédiaire d'entités logiques, appelés flux, qui représentent des objets externes au programme, appelés fichiers. Un fichier peut être ouvert en lecture, auquel cas il est censé nous fournir des données (c'est-à-dire être lu) ou ouvert en écriture, auquel cas il est destiné à recevoir des données provenant du programme. Un fichier peut être à la fois ouvert en lecture et en écriture. Une fois qu'un fichier est ouvert, un flux lui est associé. Un flux d'entrée est un flux associé à un fichier ouvert en lecture et un flux de sortie un flux associé à un fichier ouvert un écriture. Tous les fichiers ouverts doivent être fermés avant la fin du programme.

Lorsque les données échangées entre le programme et le fichier sont de type texte, la nécessité de définir ce qu'on appelle une ligne est primordiale. En langage C, une ligne est une suite de caractères terminée par le caractère de fin de ligne (inclus) : '\n'. Par exemple, lorsqu'on effectue des saisies au clavier, une ligne correspond à une suite de caractères terminée par ENTREE. Puisque la touche ENTREE termine une ligne, le caractère généré par l'appui de cette touche est donc, en C standard, le caractère de fin de ligne soit '\n'.

V-C. Les entrée et sortie standards

Lorsque le système exécute un programme, trois fichiers sont automatiquement ouverts :

  • l'entrée standard par défaut le clavier
  • la sortie standard, par défaut l'écran (ou la console)
  • et l'erreur standard, par défaut associé à l'écran (ou la console)

Respectivement associés aux flux stdin, stdout et stderr. Ils sont automatiquement fermés avant la fin du programme.

Chez la majorité des systèmes, dont Windows et UNIX, l'utilisateur peut rediriger les entrée et sortie standards vers un autre fichier à l'aide des symboles < et >. Par exemple, exécutez le programme qui affiche « Hello, world » à partir de l'interpréteur de commandes à l'aide de la commande suivante (nous supposons que nous sommes sous Windows et que le programme s'appelle hello.exe) :

hello > sortie.txt

et vous verrez que le message sera imprimé dans le fichier sortie.txt et non à l'écran. Si le fichier n'existe pas, il sera créé. S'il existe déjà, son ancien contenu sera effacé.

Un deuxième et dernier exemple : écrire un programme qui affiche Hello, world en écrivant explicitement sur stdout, autrement dit avec un printf beaucoup plus explicite. On utilisera alors la fonction fprintf qui permet d'écrire du texte sur un flux de sortie, dans notre cas : stdout, ce qui nous donne :

#include <stdio.h>

int main()
{
    fprintf(stdout, "Hello, world\n");
    return 0;
}

V-D. Exemple : lire un caractère, puis l'afficher

La macro getc permet de lire un caractère sur un flux d'entrée. La macro putc permet d'écrire un caractère sur un flux de sortie. Voici un programme simple qui montre comment utiliser les macros getc et putc :

#include <stdio.h>

int main()
{
    int c; /* le caractere */

    printf("Veuillez taper un caractere : ");
    c = getc(stdin);

    printf("Vous avez tape : ");
    putc(c, stdout);

    return 0;
}

Vous vous demandez certainement la raison pour laquelle on a utilisé int plutôt que char dans la déclaration de c. Et bien tout simplement parce que getc retourne un int (de même putc attend un argument de type int). Mais justement : Pourquoi ? Et bien parce que getc doit pouvoir non seulement retourner le caractère lu (un char) mais aussi une valeur qui ne doit pas être un char pour signaler qu'aucun caractère n'a pu être lu. Cette valeur est EOF. Elle est définie dans le fichier stdio.h. Dans ces conditions, il est clair qu'on peut utiliser tout sauf un char comme type de retour de getc.

Un des cas les plus fréquents où getc retourne EOF est lorsqu'on a rencontré la fin du fichier. La fin d'un fichier est un point situé au-delà du dernier caractère de ce fichier (si le fichier est vide, le début et la fin du fichier sont donc confondus). On dit qu'on a rencontré la fin d'un fichier après avoir encore tenté de lire dans ce fichier alors qu'on se trouve déjà à la fin, pas juste après avoir lu le dernier caractère. Lorsque stdin est associé au clavier, la notion de fin de fichier perd à priori son sens car l'utilisateur peut très bien taper n'importe quoi à n'importe quel moment. Cependant l'environnement d'exécution (le système d'exploitation) offre généralement un moyen de spécifier qu'on n'a plus aucun caractère à fournir (concrètement, pour nous, cela signifie que getc va retourner EOF). Sous Windows par exemple, il suffit de taper en début de ligne la combinaison de touches Ctrl + Z (héritée du DOS) puis de valider par ENTREE. Evidemment, tout recommence à zéro à la prochaine opération de lecture.

Les macros getchar et putchar s'utilisent comme getc et putc sauf qu'elles n'opèrent que sur stdin, respectivement stdout. Elles sont définies dans stdio.h comme suit :

#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)

Et enfin fgetc est une fonction qui fait la même chose que getc (qui peut être en fait une fonction ou une macro ...). De même fputc est une fonction qui fait la même chose que putc.

V-E. Saisir une chaîne de caractères

Il suffit de lire les caractères présents sur le flux d'entrée (dans notre cas : stdin) jusqu'à ce que l'on ait atteint la fin du fichier ou le caractère de fin de ligne. Nous devrons fournir en arguments de la fonction l'adresse du tampon destiné à contenir la chaîne de caractère saisie et la taille de ce tampon pour supprimer le risque de débordement de tampon.

#include <stdio.h>

char * saisir_chaine(char * lpBuffer, size_t nBufSize);

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    saisir_chaine(lpBuffer, sizeof(lpBuffer));

    printf("Vous avez tape : %s\n", lpBuffer);

    return 0;
}

char * saisir_chaine(char * lpBuffer, size_t nBufSize)
{
    size_t nbCar = 0;
    int c;

    c = getchar();
    while (nbCar < nBufSize - 1 && c != EOF && c != '\n')
    {
        lpBuffer[nbCar] = (char)c;
        nbCar++;
        c = getchar();
    }

    lpBuffer[nbCar] = '\0';

    return lpBuffer;
}

La fonction scanf permet également de saisir une chaîne de caractères ne comportant aucun espace (espace, tabulation, etc.) grâce au spécificateur de format %s. Elle va donc arrêter la lecture à la rencontre d'un espace (mais avant d'effectuer la lecture, elle va d'abord avancer jusqu'au premier caractère qui n'est pas un espace). scanf ajoute enfin le caractère de fin de chaîne. Le gabarit permet d'indiquer le nombre maximum de caractères à lire (caractère de fin de chaîne non compris). Lorsqu'on utilise scanf avec le spécificateur %s (qui demande de lire une chaîne sans espace), il ne faut jamais oublier de spécifier également le nombre maximum de caractères à lire (à mettre juste devant le s) sinon le programme sera ouvert aux attaques par débordement de tampon. Voici un exemple qui montre l'utilisation de scanf avec le spécicateur de format %s :

#include <stdio.h>

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    scanf("%19s", lpBuffer);

    printf("Vous avez tape : %s\n", lpBuffer);

    return 0;
}

Et enfin, il existe également une fonction, gets, déclarée dans stdio.h, qui permet de lire une chaîne de caractères sur stdin. Cependant cette fonction est à proscrire car elle ne permet pas de spécifier la taille du tampon qui va recevoir la chaîne lue.

V-F. Lire une ligne avec fgets

La fonction fgets permet de lire une ligne (c'est-à-dire y compris le '\n') sur un flux d'entrée et de placer les caractères lus dans un buffer. Cette fonction ajoute ensuite le caractère '\0'. Exemple :

#include <stdio.h>

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    fgets(lpBuffer, sizeof(lpBuffer), stdin);

    printf("Vous avez tape : %s", lpBuffer);

    return 0;
}

Dans cet exemple, deux cas peuvent se présenter :

  • l'utilisateur entre une chaîne comportant 18 caractères tout au plus puis valide le tout par ENTREE, alors tous les caractères de la ligne, y compris le caractère de fin de ligne, sont copiés dans lpBuffer puis le caractère de fin de chaîne est ajouté

  • l'utilisateur entre une chaîne comportant plus de 18 caractères (c'est-à-dire >= 19) puis valide le tout par ENTREE, alors seuls les 19 premiers caractères sont copiés vers lpBuffer puis le caractère de fin de chaîne est ajouté

V-G. Mécanisme des entrées/sorties en langage C

V-G-1. Le tamponnage

V-G-1-a. Les tampons d'entrée/sortie

En langage C, les entrées/sorties sont par défaut bufférisées, c'est-à-dire que les données à lire (respectivement à écrire) ne sont pas directement lues (respectivement écrites) mais sont tout d'abord placées dans un tampon (buffer) associé au fichier. La preuve, vous avez certainement remarqué par exemple que lorsque vous entrez des données pour la première fois à l'aide du clavier, ces données ne seront lues qu'une fois que vous aurez appuyé sur la touche ENTREE. Ensuite, toutes les opérations de lecture qui suivent se feront immédiatement tant que le caractère '\n' est encore présent dans le tampon de lecture, c'est-à-dire tant qu'il n'a pas été encore lu. Lorsque le caractère '\n' n'est plus présent dans le buffer, vous devrez à nouveau appuyer sur ENTREE pour valider la saisie, et ainsi de suite.

Les opérations d'écriture sont moins compliquées, mais il y a quand même quelque chose dont il serait totalement injuste de ne pas en parler. Comme nous l'avons déjà dit plus haut, les entrées/sorties sont par défaut bufferisées c'est-à-dire passent par un tampon. Dans le cas d'une opération d'écriture, il peut arriver que l'on souhaite à un certain moment forcer l'écriture physique des données présentes dans le tampon sans attendre que le système se décide enfin de le faire. Dans ce cas, on utilisera la fonction fflush. Nous verrons dans le paragraphe suivant un exemple d'utilisation de cette fonction.

V-G-1-b. Les modes de tamponnage

Le langage C permet de spécifier le mode de tamponnage à utiliser avec un flux donné à l'aide de la fonction setvbuf. Elle doit être appelée avant toute utilisation du fichier.

Il existe 3 modes de tamponnage des entrées/sorties :

  • Pas de tamponnage (_IONBF (no buffering)), dans lequel le flux n'est associé à aucun tampon. Les données sont directement écrites sur ou lues depuis le fichier.

  • Tamponnage par lignes (_IOLBF (line buffering)), le mode par défaut (d'après la norme), dans lequel le flux est associé à un tampon vidé que lorsqu'il est plein, lorsque le caractère de fin de ligne a été envoyé, lorsqu'une opération de lecture sur un flux en mode "Pas de tampon" a été efectuée ou lorsqu'une opération de lecture sur un flux en mode "Tamponnage par lignes" nécessite son vidage. Par exemple, en pratique, lorsque stdout est dans ce mode, toute demande de lecture sur stdin provoquera l'écriture physique des caractères encore dans le tampon. Cela permet d'avoir les questions afichées à l'écran avant que l'utilisateur puisse entrer la réponse.

  • Tamponnage strict (_IOFBF (full buffering)), dans lequel le flux associé à un tampon vidé que lorsqu'il est plein.

Dans tous les cas, la fermeture d'un fichier ouvert en écriture entraîne également l'écriture des caractères qui sont encore dans le tampon, s'il y en a (ce qui a bien évidemment lieu avant sa fermeture).

Le prototype de la fonction setvbuf est le suivant :

int setvbuf(FILE * f, char * buf, int mode, size_t size);

L'argument mode indique évidemment le mode à utiliser (_IONBF, _IOLBF ou _IOFBF) et l'argument size la taille du tampon (buf) à associer au flux. Si buf vaut NULL, un tampon de taille size sera alloué et associé au fichier. Les arguments buf et size sont évidemment ignorés lorsque mode vaut _IONBF.

Voici un exemple d'utilisation de cette fonction :

#include <stdio.h>

#define N 1

void loop(unsigned long n);

int main()
{
    setvbuf(stdout, NULL, _IOFBF, 8);

    printf("a\n"); /* Contenu du buffer : [a\n]. */
    loop(N); /* Faire une quelconque longue boucle */

    /* Le buffer n'est pas encore plein, rien ne s'affichera donc sur la sortie standard. */

    printf("b\n"); /* Contenu du buffer : [a\nb\n]. */
    fflush(stdout); /* Vider le buffer. */

    /* Le buffer n'est pas encore plein mais fflush a ete appelee. On aura donc :   */
    /* - Sur la sortie standard : + [a\nb\n].                                       */
    /* - Dans le buffer : [] (rien).                                                */

    loop(N); /* Faire une quelconque longue boucle */

    printf("azertyuiop\n");

    /* On aura :                                          */
    /* - Dans le buffer : [azertyui].                     */
    /* Le buffer est plein, le vidage s'impose. On aura : */
    /* - Sur la sortie standard : + [azertui].            */
    /* - Dans le buffer : [] (rien).                      */
    /* Il reste encore les carateres [op\n]. On aura :    */
    /* - Dans le buffer : [op\n].                         */

    loop(N); /* Faire une quelconque longue boucle */

    return 0;

    /* Au dela de l'accolade : Fin du programme.                                           */
    /* Tous les fichiers encore ouverts (dont la sortie standard) seront fermes. On aura : */
    /* - Sur la sortie standard : + [op\n].                                                */
    /* - Dans le buffer : [] (rien).                                                       */
}

void loop(unsigned long n)
{
    unsigned long i, j, end = 100000000;

    for(i = 0; i < n; i++)
        for(j = 0; j < end; j++);
}

V-G-2. Lire de manière sûre des données sur l'entrée standard▲

Tout d'abord, analysons le tout petit programme suivant :

#include <stdio.h>

int main()
{
    char nom[12], prenom[12];

    printf("Entrez votre nom : ");
    fgets(nom, sizeof(nom), stdin);

    printf("Entrez votre prenom : ");
    fgets(prenom, sizeof(prenom), stdin);

    printf("Votre nom est : %s", nom);
    printf("Et votre prenom : %s", prenom);

    return 0;
}

Dans ce programme, si l'utilisateur entre un nom comportant moins de 10 caractères puis valide par ENTREE, alors tous les caractères rentrent dans nom et le programme se déroule bien comme prévu. Par contre si l'utilisateur entre un nom comportant plus de 10 caractères, seuls les 11 premiers caractères seront copiés dans nom et des caractères sont donc encore présents dans le buffer du clavier. Donc, à la lecture du prénom, les caractères encore présents dans le buffer seront immédiatement lus sans que l'utilisateur n'ait pu entrer quoi que ce soit. Voici un deuxième exemple :

#include <stdio.h>

int main()
{
    int n;
    char c;

    printf("Entrez un nombre (entier) : ");
    scanf("%d", &n);

    printf("Entrez un caractere : ");
    scanf("%c", &c);

    printf("Le nombre que vous ave entre est : %d\n", n);
    printf("Le caractere que vous ave entre est : %c\n", c);

    return 0;
}

Lorsqu'on demande à scanf de lire un nombre, elle va déplacer le pointeur jusqu'au premier caractère non blanc, lire tant qu'elle doit lire les caractères pouvant figurer dans l'expression d'un nombre, puis s'arrêter à la rencontre d'un caractère invalide (espace ou lettre par exemple).

Donc dans l'exemple ci-dessus, la lecture du caractère se fera sans l'intervention de l'utilisateur à cause de la présence du caractère '\n' (qui sera alors le caractère lu) due à la touche ENTREE frappée pendant la saisie du nombre.

Ces exemples nous montrent bien que d'une manière générale, il faut toujours vider le buffer du clavier après chaque saisie, sauf si celui-ci est déjà vide bien sûr. Pour vider le buffer du clavier, il suffit de manger tous les caractères présents dans le buffer jusqu'à ce qu'on ait rencontré le caractère de fin de ligne ou atteint la fin du fichier. A titre d'exemple, voici une version améliorée (avec vidage du tampon d'entrée après lecture) de notre fonction saisir_chaine :

char * saisir_chaine(char * lpBuffer, int nBufSize)
{
    char * ret = fgets(lpBuffer, nBufSize, stdin);

    if (ret != NULL)
    {
        char * p = lpBuffer + strlen(lpBuffer) - 1;
        if (*p == '\n')
            *p = '\0'; /* on ecrase le '\n' */
        else
        {
            /* On vide le tampon de lecture du flux stdin */
            int c;

            do
                c = getchar();
            while (c != EOF && c != '\n');
        }
    }

    return ret;
}


melem.developpez.com :: Initiation au langage C

Initiation au langage C

Table des matières

  • VI. L'allocation dynamique de mémoire
    • VI-A. Les fonctions malloc et free
    • VI-B. La fonction realloc
    • VI-C. Exercices
      • VI-C-1. Calcul de la moyenne (version 2)
      • VI-C-2. Recherche dans un tableau (version 2)
  • VI. Solutions des exercices
    • VI-A. La lettre X (II-E-1)
    • VI-B. Périmètre d'un rectangle (II-E-2)
    • VI-C. Valeur absolue (III-G-1)
    • VI-D. Moyenne (III-G-2)
    • VI-E. L'heure dans une minute (III-G-3)
    • VI-F. Rectangle (III-G-4)
    • VI-G. Triangle isocèle (III-G-5)
    • VI-H. Somme (III-G-6)
    • VI-I. Recherche dans un tableau (IV-D-1)
    • VI-J. Calcul de la moyenne (IV-D-2)
    • VI-K. Manipulation de chaînes (IV-D-3)
    • VI-L. Calcul de la moyenne (version 2) (VI-C-1)
    • VI-M. Recherche dans un tableau (version 2) (VI-C-2)

VI. L'allocation dynamique de mémoire

VI-A. Les fonctions malloc et free

L'intérêt d'allouer dynamiquement de la mémoire se ressent lorsqu'on veut créer un tableau dont la taille dont nous avons besoin n'est connue qu'à l'exécution par exemple. On utilise généralement les fonctions malloc et free.

int t[10];
...
/* FIN */

Peut être remplacé par :

int * p;

p = malloc(10 * sizeof(int));
...
free(p); /* libérer la mémoire lorsqu'on n'en a plus besoin */
/* FIN */

Les fonctions malloc et free sont déclarées dans le fichier stdlib.h. malloc retourne NULL en cas d'échec. Voici un exemple qui illustre une bonne manière de les utiliser :

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int * p;

    /* Creation d'un tableau assez grand pour contenir 10 entiers */
    p = malloc(10 * sizeof(int));

    if (p != NULL)
    {
        printf("Succes de l'operation.\n");
        p[0] = 1;
        printf("p[0] = %d\n", p[0]);
        free(p); /* Destruction du tableau. */
    }
    else
        printf("Le tableau n'a pas pu etre cree.\n");

    return 0;
}

VI-B. La fonction realloc

La fonction realloc :

void * realloc(void * memblock, size_t newsize);

permet de « redimensionner » une mémoire allouée dynamiquement (par malloc par exemple). Si memblock vaut NULL, realloc se comporte comme malloc. En cas de réussite, cette fonction retourne alors l'adresse de la nouvelle mémoire, sinon la valeur NULL est retournée et la mémoire pointée par memblock reste inchangée.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int * p = malloc(10 * sizeof(int));

    if (p != NULL)
    {
        /* Sauver l'ancienne valeur de p au cas ou realloc echoue. */
        int * q = p;
        /* Redimensionner le tableau. */
        p = realloc(p, 20 * sizeof(int));

        if (p != NULL)
        {
            printf("Succes de l'operation.\n");
            p[0] = 1;
            printf("p[0] = %d\n", p[0]);
            free(p);
        }
        else
        {
            printf("Le tableau n'a pas pu etre redimensionne.\n");
            free(q);
        }
    }
    else
        printf("Le tableau n'a pas pu etre cree.\n");

    return 0;
}

VI-C. Exercices

VI-C-1. Calcul de la moyenne (version 2)

Reprendre l'exercice IV-D-2 en demandant cette fois-ci à l'utilisateur le nombre de notes qu'il désire entrer (limité à exactement 5 dans l'ancien sujet). Voici un exemple d'exécution :

Ce programme permet de calculer votre moyenne scolaire.
Entrez le nombre de notes que vous voulez entrer : 5
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

VI-C-2. Recherche dans un tableau (version 2)

Reprenez l'exercice IV-D-1 en remplaçant les nombres par des chaînes de caractères. L'utilisateur entrera en premier lieu le nombre de chaînes qu'il va entrer.

Aucune chaîne ne doit excéder 20 caractères mais la mémoire que vous utiliserez pour stocker chaque chaîne entrée doit être juste assez grande pour la contenir. Autrement dit, votre objectif sera de minimiser la quantité de mémoire utilisée. Voici un exemple d'exécution :

Ce programme permet de trouver toutes les occurrences d'une chaine.
Combien de chaines voulez-vous entrer ? 10
Entrez 10 chaines de caracteres (20 caracteres par chaine au plus) :
t[0] : programmation
t[1] : langage
t[2] : C
t[3] : ANSI
t[4] : ISO
t[5] : IEC
t[6] : programmation
t[7] : programme
t[8] : code
t[9] : programme
Entrez la chaine a rechercher : programmation
t[0]
t[6]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

VI. Solutions des exercices

VI-A. La lettre X (II-E-1)

Voici la solution la plus évidente et la plus lisible :

#include <stdio.h>

int main()
{
    printf("*   *\n");
    printf(" * * \n");
    printf("  *  \n");
    printf(" * * \n");
    printf("*   *\n");
    return 0;
}

On peut cependant remarquer que les espaces après le dernier * de chaque ligne sont inutiles. De plus, rien ne nous oblige à faire un appel à printf pour chaque ligne. On peut faire tout le dessin avec un seul appel à cette fonction. Voici donc une autre solution, un peu moins évidente que la première :

#include <stdio.h>

int main()
{
    printf("*   *\n * *\n  *\n * *\n*   *\n");
    return 0;
}

VI-B. Périmètre d'un rectangle (II-E-2)

#include <stdio.h>

#define LONGUEUR 100
#define HAUTEUR 60

int perimetre(int L, int h);

int main()
{
    printf(
        "Le perimetre d'un rectangle de longueur %d et de hauteur %d est %d.\n",
        LONGUEUR, HAUTEUR, perimetre(LONGUEUR, HAUTEUR)
    );
    return 0;
}

int perimetre(int L, int h)
{
    return 2 * (L + h);
}

Ayez l'habitude de représenter les valeurs figées par des macros. Cela permet d'avoir un code à la fois lisible et facilement maintenable : si un jour la spécification change, il suffit de changer les définitions des macros et non plusieurs parties du programme.

VI-C. Valeur absolue (III-G-1)

Il y a plusieurs façons d'y arriver. Voici deux exemples :

#include <stdio.h>

int main()
{
    double x, y;

    printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");
    printf("Entrez un nombre : ");
    scanf("%lf", &x);

    if (x >= 0)
        y = x;
    else
        y = -x;

    printf("La valeur absolue de %f est : %f\n", x, y);

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

#include <stdio.h>

int main()
{
    double x;

    printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");
    printf("Entrez un nombre : ");
    scanf("%lf", &x);

    printf("La valeur absolue de %f est : %f\n", x, (x >= 0 ? x : -x));

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-D. Moyenne (III-G-2)

#include <stdio.h>

int main()
{
    int note;

    printf("Ce programme permet de determiner si vous avez eu la moyenne ou non.\n");
    printf("Entrez votre note (0 a 20) : ");
    scanf("%d", &note);

    if (0 <= note && note <= 20)
    {
        if (note >= 10)
            printf("Vous avez eu la moyenne.\n");
        else
            printf("Vous n'avez pas eu la moyenne.\n");
    }
    else
        printf("Cette note n'est pas valide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-E. L'heure dans une minute (III-G-3)

#include <stdio.h>

int main()
{
    int h, m;

    printf("Ce programme permet de determiner l'heure qu'il sera une minute plus tard.\n");
    printf("Entrez l'heure actuelle : ");
    scanf("%d", &h);
    printf("Entrez la minute actuelle : ");
    scanf("%d", &m);

    m++;

    if (m == 60)
    {
        m = 0;
        h++;

        if (h == 24)
            h = 0;
    }

    printf("Dans une minute il sera : %02d:%02d\n", h, m);

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-F. Rectangle (III-G-4)

Le programme a est complet. Pour les programmes b et c, seule la fonction main() est donnée (les autres parties du programme restant inchangées par rapport à a).

a.

#include <stdio.h>

void affiche_car(char c, int n);

int main()
{
    int L;

    printf("Ce programme dessine une ligne.\n");
    printf("Entrez la longueur de la ligne (0 a 20) : ");
    scanf("%d", &L);

    if (0 <= L && L <= 20)
    {
        affiche_car('*', L);
        putchar('\n');
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

void affiche_car(char c, int n)
{
    if (c > 0 && n > 0)
    {
        int i; /* i : nombres de caracteres deja affiches */

        for(i = 0; i < n; i++) /* on arrete des que i a atteint n */
            putchar(c);
    }
}

Vous ne vous attendiez certainement pas à ce que la valeur de c soit testée avant de le passer à putchar. La raison est tout simplement que l'argument attendu par putchar est un int (qui doit avoir une valeur positive) et non un char. Comme char peut faire référence à signed char, il vaut mieux tester la valeur du caractère reçu avant de faire quoi que ce soit avec. putchar sera étudiée dans plus de détails dans le chapitre V.

b.

int main()
{
    int L;

    printf("Ce programme dessine un rectangle plein.\n");
    printf("Entrez la longueur du rectangle (0 a 20) : ");
    scanf("%d", &L);

    if (0 <= L && L <= 20)
    {
        int h;

        printf("Entrez la hauteur du rectangle (0 a 20) : ");
        scanf("%d", &h);

        if (0 <= h && h <= 20)
        {
            /* On dessine h lignes (lignes 1 a h) */

            int i; /* i : numero de la ligne en cours */

            for(i = 1; i <= h; i++)
            {
                affiche_car('*', L);
                putchar('\n');
            }
        }
        else
            printf("Cette valeur est invalide.\n");
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

c.

int main()
{
    int L;

    printf("Ce programme dessine un rectangle creux.\n");
    printf("Entrez la longueur du rectangle (0 a 20) : ");
    scanf("%d", &L);

    if (0 <= L && L <= 20)
    {
        int h;

        printf("Entrez la hauteur du rectangle (0 a 20) : ");
        scanf("%d", &h);

        if (0 <= h && h <= 20)
        {
            int i;

            for(i = 1; i <= h; i++)
            {
                /* Pour les lignes 1 et h, dessiner une ligne continue */
                if (i == 1 || i == h)
                    affiche_car('*', L);
                else
                {
                    /* Pour le reste, seules les extreimites sont visibles */
                    putchar('*');
                    affiche_car(' ', L - 2);
                    putchar('*');
                }

                putchar('\n');
            }
        }
        else
            printf("Cette valeur est invalide.\n");
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-G. Triangle isocèle (III-G-5)

Ici aussi, seule la fonction main() est donnée pour les mêmes raisons que dans VII-F.

a. Il faut juste remarquer qu'à la ligne i (i allant de 1 à h), il y a h - i espaces suivi de 1 + 2 * (i - 1) étoiles.

int main()
{
    int h;

    printf("Ce programme dessine un triangle.\n");
    printf("Entrez la hauteur du triangle (0 a 20) : ");
    scanf("%d", &h);

    if (0 <= h && h <= 20)
    {
        int i;

        for(i = 1; i <= h; i++)
        {
            affiche_car(' ', h - i);
            affiche_car('*', 2 * i - 1);
            putchar('\n');
        }
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

b.

int main()
{
    int h;

    printf("Ce programme dessine un triangle.\n");
    printf("Entrez la hauteur du triangle (0 a 20) : ");
    scanf("%d", &h);

    if (0 <= h && h <= 20)
    {
        int i, n, nb_etoiles; /* nb_etoiles : nombre d'etoiles deja affichees */

        for(i = 1, nb_etoiles = 0; i <= h; i++)
        {
            n = h - i;
            affiche_car(' ', n);

            n = 2 * i - 1;
            affiche_car('*', n);

            nb_etoiles += n;

            putchar('\n');
        }

        printf("Il y a %d etoile(s) affichee(s).\n", nb_etoiles);
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-H. Somme (III-G-6)

#include <stdio.h>

int main()
{
    int n, saisis = 0, fini = 0, total = 0;

    printf("Ce programme permet de calculer une somme.\n");
    printf("Entrez la liste des nombres a additionner (terminez en entrant 0).\n");

    while (!fini)
    {
        scanf("%d", &n);
        saisis++;
        total += n;
        fini = (saisis == 10 || n == 0);
    }

    printf("Le total est : %d\n", total);

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-I. Recherche dans un tableau (IV-D-1)

#include <stdio.h>

int main()
{
    int t[10], i, n, nb_occurrences = 0;

    printf("Ce programme permet de trouver toutes les occurrences d'un nombre.\n");
    printf("Entrez 10 nombres :\n");

    for(i = 0; i < 10; i++)
    {
        printf("t[%d] : ", i);
        scanf("%d", t + i);
    }

    printf("Entrez le nombre a rechercher : ");
    scanf("%d", &n);

    for(i = 0; i < 10; i++)
    {
        if (t[i] == n)
        {
            printf("t[%d]\n", i);
            nb_occurrences++;
        }
    }

    printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-J. Calcul de la moyenne (IV-D-2)

#include <stdio.h>

int main()
{
    int t[5][3], i, total_coefs = 0, total_notes = 0;
    double moyenne;

    printf("Ce programme permet de calculer votre moyenne scolaire.\n");

    for(i = 0; i < 5; i++)
    {
        /* Saisie des notes */

        printf("Note %d : ", i + 1);
        scanf("%d", &(t[i][0]));
        printf("Coef %d : ", i + 1);
        scanf("%d", &(t[i][1]));
        t[i][2] = t[i][0] * t[i][1];

        /* Calcul des totaux */

        total_notes += t[i][2];
        total_coefs += t[i][1];
    }

    moyenne = ((double)total_notes) / total_coefs;

    /* Affichage du resultat */

    printf("+------+------+----------+\n");
    printf("| Note | Coef | Note Def |\n");
    printf("+------+------+----------+\n");

    for(i = 0; i < 5; i++)
    {
        printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);
        printf("+------+------+----------+\n");
    }

    printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);
    printf("+------+-----------------+\n");
    printf("| Moy. | %15.2f |\n", moyenne);
    printf("+------+-----------------+\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-K. Manipulation de chaînes (IV-D-3)

char * str_cpy(char * dest, const char * source)
{
    int i;

    for(i = 0; source[i] != '\0'; i++)
        dest[i] = source[i];

    dest[i] = '\0';

    return dest;
}

size_t str_len(const char * t)
{
    size_t len;

    for(len = 0; t[len] != '\0'; len++)
        /* On ne fait rien, on laisse seulement boucler */ ;

    return len;
}

int str_equals(const char * s1, const char * s2)
{
    int i = 0, equals = 1;

    while (equals && s1[i] != '\0' && s2[i] != '\0')
    {
        if (s1[i] != s2[i])
            equals = 0;
        i++;
    }

    if (equals && (s1[i] != '\0' || s2[i] != '\0'))
        equals = 0;

    return equals;
}

Remarquez bien que nous avons choisi char * comme type de retour de str_cpy et non void. La valeur retournée n'est autre que la valeur du pointeur dest. Le prototype de str_cpy est strictement le même que celui de la fonction standard strcpy. Cela permet d'écrire du code compact lorsqu'on a envie, par exemple :

Sélectionnez
char s[50];
strcat(strcpy(s, "Bonjour"), " tout le monde !");

Le deuxième argument de la fonction (s2) est déclaré const char * et non simplement char *. En effet, les données pointées par s2 doivent être uniquement lues par la fonction, cette dernière ne doit pas avoir le droit de les modifier. L'ajout du qualificateur const ici est donc plus que recommandé. De plus, cela a un aspect documentaire : la présence ou l'abscence de const permet aux utilisateurs d'une fonction de savoir quelles données pourraient être modifiées par la fonction et quelles données seront justes lues.

VI-L. Calcul de la moyenne (version 2) (VI-C-1)

#include <stdio.h>
#include <stdlib.h>

typedef int NOTE[3];

int main()
{
    int n;

    printf("Ce programme permet de calculer votre moyenne scolaire.\n");
    printf("Entrez le nombre de notes que vous voulez entrer : ");
    scanf("%d", &n);

    if (n > 0)
    {
        NOTE * t = malloc(n * sizeof(NOTE));

        if (t != NULL)
        {
            int i, total_coefs = 0, total_notes = 0;
            double moyenne;

            for(i = 0; i < n; i++)
            {
                printf("Note %d : ", i + 1);
                scanf("%d", &(t[i][0]));
                printf("Coef %d : ", i + 1);
                scanf("%d", &(t[i][1]));
                t[i][2] = t[i][0] * t[i][1];
                total_notes += t[i][2];
                total_coefs += t[i][1];
            }

            moyenne = ((double)total_notes) / total_coefs;

            printf("+------+------+----------+\n");
            printf("| Note | Coef | Note Def |\n");
            printf("+------+------+----------+\n");

            for(i = 0; i < n; i++)
            {
                printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);
                printf("+------+------+----------+\n");
            }

            free(t);

            printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);
            printf("+------+-----------------+\n");
            printf("| Moy. | %15.2f |\n", moyenne);
            printf("+------+-----------------+\n");
        }
        else
            printf("Le tableau n'a pas pu etre cree.\n");
    }
    else
        printf("Cette valeur est invalide.\n");

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

VI-M. Recherche dans un tableau (version 2) (VI-C-2)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef char * PCHAR;

size_t get_string(char * lpBuffer, int nBufSize);

int main()
{
    int n;
    char s[21];

    printf("Ce programme permet de trouver toutes les occurrences d'une chaine.\n");
    printf("Combien de chaines voulez-vous entrer ? ");
    scanf("%d", &n);
    get_string(s, sizeof(s)); /* Pour purger stdin */

    if (n > 0)
    {
         PCHAR * t = malloc(n * sizeof(PCHAR));

         if (t != NULL)
         {
            int i, nb_occurrences = 0, succes = 1;

            printf("Entrez %d chaines de caracteres (20 caracteres par chaine au plus) :\n", n);

            for(i = 0; succes && i < n; i++)
            {
                size_t len;

                printf("t[%d] : ", i);
                len = get_string(s, sizeof(s));
                t[i] = malloc(len + 1);

                if (t[i] != NULL)
                    strcpy(t[i], s);
                else
                {
                    int j;

                    for(j = 0; j < i; j++)
                        free(t[j]);

                    succes = 0;

                    printf("Memoire insuffisante pour continuer.\n");
                }
            }

            if (succes)
            {
                printf("Entrez la chaine a rechercher : ");
                get_string(s, sizeof(s));

                for(i = 0; i < n; i++)
                {
                    if (strcmp(t[i], s) == 0)
                    {
                        printf("t[%d]\n", i);
                        nb_occurrences++;
                    }

                    free(t[i]);
                }

                printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);
            }

            free(t);
         }
         else
            printf("Le tableau n'a pas pu etre cree.\n");
    }

    printf("Merci d'avoir utilise ce programme. A bientot !\n");

    return 0;
}

size_t get_string(char * lpBuffer, int nBufSize)
{
    size_t len = 0;

    if (fgets(lpBuffer, nBufSize, stdin) != NULL)
    {
        char * p;
        len = strlen(lpBuffer);

        p = lpBuffer + len - 1;
        if (*p == '\n')
        {
            *p = '\0'; /* on ecrase le '\n' */
            len--;
        }
        else
        {
            /* On vide le tampon de lecture du flux stdin */
            int c;

            do
                c = getchar();
            while (c != EOF && c != '\n');
        }
    }

    return len;
}


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

I. Introduction

I-A. Historique

L'histoire du langage C est intimement liée à celle du système d'exploitation UNIX. En 1969, Ken Thompson, qui travaillait pour Bell Laboratories, mit au point sur un DEC PDP-7 un système d'exploitation semblable à Multics mais plus simple et plus modeste. Ce système reçut par la suite le nom d'Unics (Uniplexed Information and Computing System) qui ne tarda pas à être définitivement changé en UNIX.

A l'époque, le seul langage vraiment adapté à l'écritute de systèmes d'exploitation était le langage d'assemblage. Ken Thompson développa alors un langage de plus haut niveau (c'est-à-dire plus proche du langage humain), le langage B (dont le nom provient de BCPL, un sous-ensemble du langage CPL, lui-même dérivé de l'Algol, un langage qui fut populaire à l'époque), pour faciliter l'écriture d'UNIX. C'était un langage faiblement typé (un langage non typé, par opposition à un langage typé, est un langage qui manipule les objets sous leur forme binaire, sans notion de type (caractère, entier, réel, ...)) et trop dépendant du PDP-7 pour permettre de porter UNIX sur d'autres machines. Alors Denis Ritchie et Brian Kernighan, de Bell Laboratories également, améliorèrent le langage B pour donner naissance au langage C. En 1973, UNIX fut réécrit entièrement en langage C. Pendant 5 ans, le langage C fut limité à l'usage interne de Bell jusqu'au jour ou Brian Kernighan et Denis Ritchie publièrent une première définition du langage dans un ouvrage intitulé The C Programming Language. Ce fut le début d'une révolution dans le monde de l'informatique.

Grâce à sa puissance, le langage C devint rapidement très populaire et en 1983, l'ANSI (American National Standards Institute) décida de le normaliser en ajoutant également quelques modifications et améliorations, ce qui donna naissance en 1989 au langage tel que nous le connaissons aujourd'hui. L'année suivante, le langage C a été adopté par l'ISO (International Organization for Standardization) et actuellement, par l'ISO et l'IEC (International Electrotechnical Commission), et a connu au fil du temps certaines révisions (dans l'immense majorité des cas toujours compatibles avec les versions antérieures) dont les plus importantes sont celles de 1990 (adoption par l'ISO), de 1995 (amélioration du support du développement de programmes internationnaux) et de 1999 (ajout d'un nombre important de nouvelles fonctionnalités). La version de 1999 est cependant actuellement encore peu diffusée c'est pourquoi dans ce document, nous nous placerons dans l'apprentissage des versions de 1990 et 1995.

I-B. Caractéristiques du langage C

I-B-1. Universalité

Langage de programmation par excellence, le C n'est pas confiné à un domaine particulier d'applications. En effet, le C est utilisé dans l'écriture

  • de systèmes d'exploitations (comme Windows, UNIX et Linux) ou de machines virtuelles (JVMs, Runtimes et Frameworks .NET, logiciels de virtualisation, etc.)
  • de logiciels de calcul scientifique, de modélisation mathématique ou de CFAO (Matlab, R, Labview, Scilab, etc.)
  • de bases de données (MySQL, Oracle, etc.)
  • d'applications en réseau (applications intranet, internet, etc.)
  • de jeux vidéo (notamment avec OpenGL ou la SDL et/ou les différents moteurs (de jeu, physique ou 3D) associés))
  • d'assembleurs, compilateurs, débogueurs, interpréteurs, de logiciels utilitaires et dans bien d'autres domaines encore.

Oui, le C permet tout simplement de tout faire, ce qui lui a valu, à juste titre, l'appelation courante "the God's programming language" (le langage de programmation de Dieu). A cause de son caractère proche du langage de la machine, le C est cependant peu productif, ce qui signifie qu'il faut souvent écrire beaucoup pour faire peu. C'est donc le prix à payer pour l'utilisation de ce langage surpuissant !

I-B-2. Concision, souplesse

C'est un langage concis, très expressif, et les programmes écrits dans ce langage sont très compacts grâce à un jeu d'opérateurs puissant.

I-B-3. Puissance

Le C est un langage de haut niveau mais qui permet d'effectuer facilement des opérations de bas niveau et d'accéder aux fonctionnalités du système, ce qui est la plupart du temps impossible ou difficile à réaliser dans les autres langages de haut niveau. C'est d'ailleurs ce qui fait l'originalité du C, et aussi la raison pour laquelle il est autant utilisé dans les domaines où la performance est cruciale comme la programmation système, le calcul numérique ou encore l'informatique embarquée.

I-B-4. Portabilité

C'est un langage qui ne dépend d'aucune plateforme matérielle ou logicielle, c'est-à-dire qui est entièrement portable. De plus, de par sa simplicité, écrire un compilateur C pour un processeur donné n'est pas beaucoup plus compliqué que d'écrire un assembleur pour ce même processeur. Ainsi, là où l'on dispose d'un assembleur pour programmer, on dispose aussi généralement d'un compilateur C, d'où l'on dit également que le C est un "assembleur portable".

I-B-5. Omniprésence

La popularité du langage mais surtout l'élégance des programmes écrits en C est telle que son style et sa syntaxe ont influencé de nombreux langages :

  • C++ et Objective C sont directement descendus du C (on les considère souvent comme des extensions du C)
  • C++ a influencé Java
  • Java a influencé JavaScript
  • PHP est un mélange de C et de langage de shell-scripting sous Linux
  • C# est principalement un mix de C++ et de Java, deux langages de style C, avec quelques influences fonctionnelles d'autres langages comme Delphi et Visual Basic (le # de C# vient-il dit-on de deux "++" superposés)
  • Et on risque d'y passer la journée ...

Connaître le C est donc très clairement un atout majeur pour quiconque désire apprendre un de ces langages.

I-C. A propos de ce document

Ce document contient des cours et des exercices. Chaque fois que vous tombez sur des exercices, traitez-les avant de passer au chapitre ou au paragraphe suivant.


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

Table des matières

  • II. Instruction, programme et fonction
    • II-A. Structure générale d'un programme C
      • II-A-1. Un premier programme
      • II-A-2. Les commentaires
      • II-A-3. Sensibilité à la casse
      • II-A-4. Le format libre
    • II-B. Du fichier source à l'exécutable
      • II-B-1. La compilation
      • II-B-2. Applications consoles
      • II-B-3. Manipulations avec Visual C++ et Code::Blocks
        • II-B-3-a. Avec Visual C++ 6
        • II-B-3-b. Avec Visual Studio .NET
        • II-B-3-c. Avec Code::Blocks 1.0
    • II-C. Les fonctions
      • II-C-1. Introduction
      • II-C-2. Exemple avec une fonction "mathématique"
      • II-C-3. Exemple avec une "procédure"
      • II-C-4. Remarques
    • II-D. Les macros
      • II-D-1. Le préprocesseur
      • II-D-2. Définition
    • II-E. Exercices
      • II-E-1. La lettre X
      • II-E-2. Périmètre d'un rectangle

II. Instruction, programme et fonction

II-A. Structure générale d'un programme C

II-A-1. Un premier programme

Nous allons écrire un programme qui affiche Hello, world en langage C.

hello.c
#include <stdio.h>
int main()
{
    printf("Hello, world\n");
    return 0;
}

Voici la sortie de ce programme (c'est-à-dire, ce que nous, utilisateurs, verrons affiché sur le périphérique de sortie standard) :

Hello, world

Le caractère '\n' dans la chaîne de caractères "Hello, world\n" représente le caractère de fin de ligne. Il permet d'insérer une nouvelle ligne (new line).

En langage C, la « brique » qui permet de créer un programme est la fonction. Un programme écrit en C est donc constitué d'une ou plusieurs fonctions, une fonction étant généralement composée d'une ou plusieurs instructions, chaque instruction élémentaire devant se terminer par un point-virgule.

printf est une fonction qui permet d'afficher du texte sur la sortie standard, par défaut l'écran. main est également une fonction, c'est celle qui sera automatiquement appelée à l'exécution. On l'appelle le point d'entrée du programme ou encore la fonction principale. Ici, elle est seulement composée de deux instructions :

  1. printf("Hello, world\n");
  2. return 0;

Selon la norme officielle du langage C, main est une fonction qui doit retourner un entier (int). Chez de nombreux systèmes (dont Windows et UNIX), cet entier est appelé le code d'erreur de l'application. En langage C, bien que cela ne soit pas forcément le cas pour le système d'exploitation, on retourne 0 pour dire que tout s'est bien passé.

Le langage C impose (à quelques exceptions près que nous verrons plus bas) qu'une fonction doit avoir été déclarée avant de pouvoir être utilisé (nous verrons plus tard ce que c'est qu'une déclaration). Dans notre exemple, puisque nous utilisons la fonction printf, nous devons tout d'abord la déclarer. Le moyen le plus simple de le faire est d'inclure le fichier stdio.h, qui contient entre autres la déclaration de cette fonction, à l'aide de la directive include :

#include <stdio.h>

II-A-2. Les commentaires

On peut insérer des commentaires n'importe où dans du code C à l'aide des délimiteurs /* et */. Les commentaires permettent de rendre les programmes plus lisibles. Voici un exemple d'utilisation de commentaires :

hello.c
#include <stdio.h>
int main()
{
    /* Ce programme affiche "Hello, world" */
    printf("Hello, world\n");
    return 0;
}

II-A-3. Sensibilité à la casse

Le C est sensible à la casse : il fait la distinction entre les majuscules et les minuscules. Printf et printf sont deux choses complètement différentes et qui n'ont strictement rien à voir ...

II-A-4. Le format libre

En langage C, les espaces (y compris les tabulations et les retours à la ligne) peuvent être utilisées à volonté (ou pas du tout) avant ou après un séparateur (# < > ( ) { } ; …). On traduit ce fait en disant que le C est un langage au format libre. Une chaîne ("...") doit cependant toujours tenir sur une seule ligne (nous reviendrons sur ce point plus tard). Ainsi, notre programme qui affiche "Hello, world" aurait également pu s'écrire :

#   include    <stdio.h>
int
main ( )
{   printf    (
        "Hello, world\n"
    );return

    0

;}

Dans certains cas, les espaces peuvent cependant être utilisés de manière précise. Par exemple, avec :

#include < stdio.h >

Vous demandez d'inclure le fichier <espace>stdio.h<espace> (qui a de fortes chances de ne pas exister, ce qui génèrerait des erreurs à la compilation ...) et non le fichier stdio.h !

II-B. Du fichier source à l'exécutable

II-B-1. La compilation

Avant de définir la compilation, il y a deux expressions qu'il faut déjà avoir défini. Il s'agit de "fichier source" et "fichier exécutable".

  • Un fichier source est un fichier contenant, dans le cas qui nous intéresse, des lignes de code écrits en C. Le code contenu dans le fichier source est appelé du code source. Les fichiers sources C portent généralement l'extension ".c" (par exemple : hello.c).

  • Un fichier exécutable est un fichier contenant, entre autres, du code directement exécutable par le processeur. Ce code est appelé code machine. L'extension portée par les fichiers exécutables varient selon le système. Sous Windows par exemple, ils portent généralement l'extension ".exe" (par exemple : hello.exe). Sous Linux, ils ne portent généralement pas d'extension.

La compilation c'est, en première approximation, le processus pendant lequel le fichier source (qui ne contient que du texte) est transformé en fichier exécutable (qui contient du code compréhensible par le processeur). Une définition plus précise sera donnée dans les cours plus avancés.

Puisque les programmes C doivent être compilés avant de pouvoir être exécutés, on dit que le C est un langage compilé. Le programme qui effectue la compilation est appelé compilateur.

Nombreux programmeurs utilisent cependant un EDI (Environnement de Développement Intégré) au lieu d'un simple compilateur pour compiler leurs programmes. Un EDI est un logiciel qui intègre un éditeur de texte pour taper les codes sources, un compilateur pour les traduire en exécutable ainsi que d'autres outils aidant à la mise au point et à la distribution des programmes. Il existe de nombreux EDIs pour le langage C. Certains sont gratuits (Code::Blocks Studio, Visual C++ Express, etc.), d'autres payants (CodeGear RAD Studio, Microsoft Visual Studio, etc.). Téléchargez ou achetez et installez le logiciel qui vous convient donc avant de continuer. Pour les manipulations pratiques, nous supposerons dans la suite que nous travaillons sous Windows, mais que cela ne vous empêche de lire ce tutoriel même si vous utilisez un autre système.

II-B-2. Applications consoles

Le C voulant être un langage entièrement portable, la création d'interfaces graphiques n'est pas supportée en standard par le langage. En effet, la notion d'interface graphique n'existe pas forcément dans tous les systèmes. Dans les systèmes proposant un environnement graphique, comme Windows par exemple, les applications qui en sont dépourvues sont souvent appelées applications consoles. Ces applications, quand elles sont lancées, s'exécutent dans une fenêtre (la fameuse "console") dans laquelle l'utilisateur n'utilise que le clavier pour commander le programme). Dans ce tutoriel, c'est toujours ce type d'applications que allons développer.

II-B-3. Manipulations avec Visual C++ et Code::Blocks

II-B-3-a. Avec Visual C++ 6
  1. Lancez Visual C++.

  2. Créez un projet d'application console que vous allez nommer helloworld en choisissant File > New > Project > Win32 Console Application. Un dossier nommé helloworld est alors créé. Ce sera votre répertoire par défaut. Deux fichiers, entre autres, sont également créés : helloworld.prj (votre projet) et helloworld.dsw (votre espace de travail).

  3. Choisissez An empty project pour qu'aucun fichier ne soit automatiquement ajouté à notre projet.

  4. Ajoutez au projet un nouveau fichier que vous allez nommer hello.c avec la commande Project > Add to Project > New > C++ source file. Nommez-le bien hello.c car si vous omettez l'extension, VC6 va automatiquement ajouter l'extension .cpp et vous aurez donc un fichier source C++ (hello.cpp) au lieu d'un fichier source C, à moins bien sûr que c'est justement ce que vous cherchiez ...

  5. Dans l'explorateur de projet (normalement c'est sur votre gauche), cliquez sur File View pour afficher une vue des fichiers qui constituent votre projet. Ouvrez ensuite le dossier Source Files puis double cliquez sur hello.c pour l'ouvrir dans l'éditeur.

  6. Saisissez maintenant le code puis compilez avec la commande Build > Build helloworld.exe (F7). Le fichier helloworld\Debug\helloworld.exe est alors créé.

  7. Pour tester votre programme sans quitter VC, choisissez Build > Execute helloworld.exe (Ctrl + F5). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.
II-B-3-b. Avec Visual Studio .NET

La procédure est presque les même que sous VC6. Créez un nouveau projet d'application Windows (Win32 Project) puis dans l'étape Applications Settings de l'assistant, choissez Console Application puis Empty Project. Compilez puis testez.

II-B-3-c. Avec Code::Blocks 1.0
  1. Lancez Code::Blocks.

  2. Créez un nouveau projet d'application console que vous aller nommer helloworld en choisissant File > New Project > Console Application. Enregistrez-le dans un répertoire de votre choix qui sera alors votre répertoire par défaut. Avant de valider, cochez l'option Do not create any files afin qu'aucun fichier ne soit automatiquement ajouté à notre projet. Une fois que vous avez validé, un fichier helloworld.cbp (votre projet) sera créé dans le répertoire de votre projet.

  3. Créez un nouveau fichier que vous allez nommer hello.c avec la commande File > New File. Acceptez que ce fichier soit ajouté au projet.

  4. Saisissez maintenant le code puis compilez avec la commande Build > Build (Ctrl + F9). Le fichier helloworld.exe est alors créé.

  5. Pour tester votre programme sans quitter Code::Blocks, choisissez Build > Run (Ctrl + F10). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.

II-C. Les fonctions

II-C-1. Introduction

Comme nous l'avons déjà dit plus haut, les fonctions sont les briques avec lesquelles on construit un programme en langage C. La vie d'un programmeur C se résume donc à créer puis utiliser des fonctions. Vous allez adorer ! Nous allons donc maintenant voir de plus près ce que c'est qu'une fonction.

II-C-2. Exemple avec une fonction "mathématique"

En mathématiques, on définit une fonction comme suit :

f(x) = x² - 3

Cela signifie que f est une fonction qui reçoit en argument un réel x et qui retourne un réel : x2 - 3.

Ecrivons une fonction C que nous appellerons f, qui reçoit en argument un entier x et qui retourne également un entier : x2 - 3.

int f(int x)
{
    return x*x - 3;
}

Le code ci-dessus constitue ce qu'on appelle la définition ou encore l'implémentation de la fonction f. Voyons maintenant un exemple d'utilisation de cette fonction.

#include <stdio.h>
int f(int); /* declaration de la fonction f */
int main()
{
    int x = 4;
    printf("f(%d) = %d\n", x, f(x));
    return 0;
}
int f(int x)
{
    return x*x - 3;
}

La ligne :

int f(int);

tient lieu de déclaration de la fonction f. Sans le point-virgule, on a ce qu'on appelle le prototype ou la signature de la fonction f. Ici, le prototype (la signature) indique que "f est une fonction qui nécessite en argument un int (int f(int)) et qui retourne un int (int f(int))". Un prototype suivi d'un point-virgule constitue ce qu'on appelle une déclaration (mais ce n'est pas la seule forme possible de déclaration d'une fonction).

Attention ! Nous avons vu jusqu'ici qu'une instruction élémentaire se termine par un point-virgule. Cela ne signifie en aucun cas que tout ce qui se termine par un point-virgule est donc une « instruction ». Une déclaration est une déclaration, pas une instruction. Nous définirons ce que c'est exactement une instruction encore plus loin.

Le %d dans la chaîne passée en premier argument de printf est ce qu'on appelle un spécificateur de format. Il renseigne sur la manière dont nous souhaitons afficher le texte. Ici, on veut afficher les nombres 4 et 13 (f(4)). Nous disons donc à printf d'utiliser le format « nombre entier » (%d) pour les afficher. Le premier %d correspond au format que nous voulons utiliser pour afficher x et le deuxième pour f(x). Nous y reviendrons un peu plus loin.

Sachez également que la variable x dans la fonction main n'a absolument rien à voir avec la variable x en paramètre de la fonction f. Chaque fonction peut avoir ses propres variables et ignore complètement ce qui se passe chez les autres fonctions. On vient ainsi d'introduire le concept de "variables locales" que nous étudierons également plus tard.

II-C-3. Exemple avec une "procédure"

Certains langages de programmation (Pascal, VB (Visual Basic), etc.) font la distinction entre "fonction" et "procédure". En langage C et ses dérivés, il n'y a pas cette distinction. Ce que chez certains langages on appelle procédure est en effet ni plus ni moins qu'une fonction qui effectue un traitement mais qui ne retourne pas de valeur, c'est-à-dire une fonction qui ne retourne rien. En langage C, ce "rien" est indiqué par le mot-clé void (terme anglais signifiant "vide"). Voici un exemple de fonction qui ne requiert aucun argument et qui ne retourne rien : une fonction qui affiche "Bonjour." 3 fois (en 3 lignes).

#include <stdio.h>
void Bonjour3Fois(void);
int main()
{
    /* Ce programme affiche Bonjour 6 fois */
    Bonjour3Fois();
    Bonjour3Fois();
    return 0;
}
void Bonjour3Fois(void)
{
    printf("Bonjour.\n");
    printf("Bonjour.\n");
    printf("Bonjour.\n");
}

II-C-4. Remarques

Dans une déclaration on peut :

  • Mettre le nom des arguments de la fonction (bon uniquement pour la déco et rien d'autre). Par exemple :
    exemple de déclaration d'une fonction

int Surface(int Longueur, int largeur);
  • Ne pas préciser les arguments de la fonction (déconseillé). Dans ce cas, il faut aller à la définition de la fonction pour connaître les arguments qu'elle requiert. Attention ! Ne pas préciser les arguments d'une fonction n'est pas la même chose que déclarer une fonction qui ne requiert aucun argument. Exemple :
    exemple de déclaration incomplète d'une fonction (déconseillé)

int Surface(); /* Surface est une fonction, point. Voir sa definition pour plus de details. */
    exemple de déclaration d'une fonction qui ne requiert aucun argument

int Surface(void);

De plus on peut :

  • Ne pas déclarer une fonction retournant un int (déconseillé).

  • La déclaration d'une fonction n'est nécessaire que lorsque son utilisation précède sa définition. En effet, une fois définie, la fonction est entièrement connue et donc n'a plus besoin d'être déclarée. Il est cependant toujours conseillé de ne définir une fonction qu'après son utilisation (ce qui requiert donc une déclaration) ne serait-ce que pour la lisibilité du programme (en effet c'est le programme qu'on veut voir à première vue, pas les petits détails).

Dans une définition:

  • On peut ne pas préciser le type de retour d'une fonction (déconseillé). Dans ce cas celle-ci est supposée retourner un int.

  • Si la fonction a déjà été déclarée à l'aide d'une déclaration complète (c'est-à-dire avec prototype), une paire de parenthèses vide signifie que la fonction n'accepte aucun argument. Sinon, les parenthèses vides signifient que les arguments requis par la fonction ne sont pas renseignés. Cette manière de définir une fonction est à éviter sauf pour des fonctions qui ne sont jamais appelées depuis le programme comme main() par exemple.

II-D. Les macros

II-D-1. Le préprocesseur

Avant d'être effectivement compilés, les fichiers sources C sont traités par un préprocesseur qui résout certaines directives qui lui sont données comme l'inclusion de fichier par exemple. Le préprocesseur, quoi qu'étant un programme à priori indépendant du compilateur, est un élément indispensable du langage.

Une directive donnée au préprocesseur commence toujours par #. Nous avons déjà rencontré la directive include qui permet d'inclure un fichier. La directive define permet de définir des macros.

II-D-2. Définition

Une macro, dans sa forme la plus simple, se définit de la manière suivante :

#define <macro> <le texte de remplacement>

Par exemple :

#define N 10

commande au préprocesseur de remplacer toutes les occurrences de le lettre N par 10. Bien sûr il n'y a pas que les lettres et les nombres qu'on peut utiliser, rien ne nous empêche par exemple d'écrire :

#define PLUS +

Ainsi, toutes les occurrences de PLUS seront remplacés par +.

Dans certaines circonstances, les macros peuvent également avantageusement remplacer les fonctions. Par exemple :

#define carre(x) x * x

Dans ce cas, une expression telle que

carre(3)

sera remplacé par :

3 * 3

Si cela semble fonctionner à première vue, voyons un peu ce qu'on obtiendrait avec :

carre(1 + 1)

Et bien, on obtiendrait :

1 + 1 * 1 + 1

soit 1 + 1 + 1 soit 3 car la multiplication est prioritaire par rapport à l'addition. Nous verrons plus tard l'ordre de priorité des opérateurs. C'est pourquoi on utilise généralement beaucoup de parenthèses dans le corps d'une macro. Ainsi, dans l'exemple précédent, on aurait du par exemple écrire :

#define carre(a) ((a) * (a))

Ce qui nous donnerait pour carre(1 + 1) par exemple :

((1 + 1) * (1 + 1))

II-E. Exercices

II-E-1. La lettre X

Ecrire un programme qui dessine la lettre dans une grille de dimensions 5 x 5 en utilisant des espaces et le caractère '*'. Cela signifie qu'on veut avoir comme sortie :

*   *
 * *
  *
 * *
*   *

II-E-2. Périmètre d'un rectangle

Ecrire une fonction qui permet d'avoir le périmètre d'un rectangle de longueur et largeur données en arguments et utiliser cette fonction dans un programme arbitraire pour afficher le périmètre d'un rectangle de dimensions 100 x 60. Exemple de sortie :

Le perimetre d'un rectangle de longueur 100 et de largeur 60 est 320.


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

Table des matières

  • III. Expressions et instructions
    • III-A. Introduction aux types de données du langage C
      • III-A-1. Les types de base du langage C
      • III-A-2. Règle d'écriture des constantes littérales
        • Nombres entiers
        • Nombres flottants
        • Caractères et chaînes de caractères
      • III-A-3. Spécification de format dans printf
        • III-A-3-a. Généralités
        • III-A-3-b. Les "options"
        • III-A-3-c. Le type entier
        • III-A-3-d. Le type flottant
      • III-A-4. Les variables et les constantes
      • III-A-5. Définition de nouveaux types
    • III-B. Les pointeurs
      • III-B-1. Définition
      • III-B-2. Saisir des données tapées au clavier avec la fonction scanf
      • III-B-3. Exemple de permutation des contenus de deux variables
    • III-C. Les expressions
      • III-C-1. Introduction
      • III-C-2. lvalue et rvalue
      • III-C-3. Opérations usuelles
        • III-C-3-a. Les opérateurs arithmétiques courants
        • III-C-3-b. Les opérateurs de comparaison
        • III-C-3-c. Les opérateurs logiques
      • III-C-4. L'opérateur virgule
      • III-C-5. Taille des données. L'opérateur sizeof
      • III-C-6. Les opérateurs d'incrémentation et de décrémentation
      • III-C-7. Expressions conditionnelles
      • III-C-8. Autres opérateurs d'affectation
      • III-C-9. Ordre de priorité des opérateurs
    • III-D. Considérations liées à la représentation binaire
      • III-D-1. Généralités
      • III-D-2. Les caractères
      • III-D-3. Dépassement de capacité
    • III-E. La conversion de type
      • III-E-1. Conversion implicite
      • III-E-2. Conversion explicite (cast)
    • III-F. Les instructions
      • III-F-1. Introduction
      • III-F-2. Bloc d'instructions
      • III-F-3. L'instruction if
      • III-F-4. L'instruction do
      • III-F-5. L'instruction while
      • III-F-6. L'instruction for
      • III-F-7. Les instructions switch et case
      • III-F-8. L'instruction break
      • III-F-9. L'instruction continue
      • III-F-10. L'instruction return
      • III-F-11. L'instruction vide
    • III-G. Exercices
      • III-G-1. Valeur absolue
      • III-G-2. Moyenne
      • III-G-3. L'heure dans une minute
      • III-G-4. Rectangle
      • III-G-5. Triangle isocèle
      • III-G-6. Somme

    • III. Expressions et instructions

    III-A. Introduction aux types de données du langage C

    III-A-1. Les types de base du langage C

    Au niveau du processeur, toutes les données sont représentées sous leur forme binaire et la notion de type n'a pas de sens. Cette notion n'a été introduite que par les langages de haut niveau dans le but de rendre les programmes plus rationnels et structurés. Mais même parmi les langages de haut niveau, on distingue ceux qui sont dits fortement typés (qui offrent une grande variété de types), comme le Pascal par exemple, de ceux qui sont dits faiblement ou moyennement typés (et plus proches de la machine) comme le C par exemple. Le langage C ne dispose que de 4 types de base :

    Type C | Type correspondant
char | caractère (entier de petite taille)
int | entier
float | nombre flottant (réel) en simple précision
double | nombre flottant (réel) en double précision

    Devant char ou int, on peut mettre le modificateur signed ou unsigned selon que l'on veut avoir un entier signé (par défaut) ou non signé. Par exemple :

    char ch;
unsigned char c;
unsigned int n; /* ou tout simplement : unsigned n */

    La plus petite valeur possible que l'on puisse affecter à une variable de type entier non signé est 0 alors que les entiers signés acceptent les valeurs négatives.

    Devant int, on peut mettre également short ou long auxquels cas on obtiendrait un entier court (short int ou tout simplement short) respectivement un entier long (long int ou tout simplement long). Voici des exemples de déclarations valides :

    int n = 10, m = 5;
short a, b, c;
long x, y, z = -1;
unsigned long p = 2;

    long peut être également mis devant double, le type résultant est alors long double (quadruple précision).

    III-A-2. Règle d'écriture des constantes littérales

    Nombres entiers

    Toute constante littérale « pure » de type entier (ex : 1, -3, 60, 40, -20, ...) est considérée par le langage comme étant de type int.

    Pour expliciter qu'une constante littérale de type entier est de type unsigned, il suffit d'ajouter à la constante le suffixe u ou U. Par exemple : 2u, 30u, 40U, 50U, ...

    De même, il suffit d'ajouter le suffixe l ou L pour expliciter qu'une constante littérale de type entier est de type long (on pourra utiliser le suffixe UL par exemple pour unsigned long).

    Une constante littérale de type entier peut également s'écrire en octal (base 8) ou en hexadécimal (base 16). L'écriture en hexa est évidement beaucoup plus utilisée.

    Une constante littérale écrite en octal doit être précédée de 0 (zéro). Par exemple : 012, 020, 030UL, etc.

    Une constante littérale écrite en hexadécimal doit commencer par 0x (zéro x). Par exemple 0x30, 0x41, 0x61, 0xFFL, etc.

    Nombres flottants

    Toute constante littérale « pure » de type flottant (ex : 0.5, -1.2, ...) est considérée comme étant de type double.

    Le suffixe f ou F permet d'expliciter un float. Attention, 1f n'est pas valide car 1 est une constante entière. Par contre 1.0f est tout à fait correcte. Le suffixe l ou L permet d'expliciter un long double.

    Une constante littérale de type flottant est constituée, dans cet ordre :

    • d'un signe (+ ou -)
    • d'une suite de chiffres décimaux : la partie entière
    • d'un point : le séparateur décimal
    • d'une suite de chiffres décimaux : la partie décimale
    • d'une des deux lettres e ou E : symbole de la puissance de 10 (notation scientifique)
    • d'un signe (+ ou -)
    • d'une suite de chiffres décimaux : la puissance de 10

    Par exemple, les constantes littérales suivantes représentent bien des nombres flottants : 1.0, -1.1f, 1.6E-19, 6.02e23L, 0.5 3e8

    Caractères et chaînes de caractères

    Les caractères sont placés entre simple quottes (ex : 'A', 'b', 'c', ...) et les chaînes de caractères entre double quottes (ex : "Bonjour", "Au revoir", ...). Certains caractères sont spéciaux et il faut utiliser la technique dite d'échappement pour les utiliser. En langage C, le caractère d'échappement est le caractère \. Exemples :

    Caractère | Valeur
'\t' | Le caractère 'tabulation'
'\r' | Le caractère 'retour chariot'
'\n' | Le caractère 'fin de ligne'
'\\' | Le caractère 'antislash'

    III-A-3. Spécification de format dans printf

    III-A-3-a. Généralités

    Voici la liste des codes format que nous utiliserons les plus souvent par la suite :

    Code format | Utilisation
c | Afficher un caractère
d | Afficher un int
u | Afficher un unsigned int
x, X | Afficher un entier dans le format hexadécimal
f | Afficher un float ou un double en notation décimale
e | Afficher un float ou un double en notation scientifique avec un petit e
E | Afficher un float ou un double en notation scientifique avec un grand E
g, G | Afficher un float ou un double (utilise le format le plus adapté)
% | Afficher le caractère '%'

    Lorsqu'on affiche une donnée de type flottant dans le format "scientifique", il n'y a qu'un chiffre, ni moins ni plus, devant le point décimal (et ce chiffre est 0 si et seulement si la donnée vaut 0), le nombre de chiffres après ce point est variable et le nombre de chiffres utilisés pour représenter l'exposant dépend de l'implémentation mais il y en a toujours au moins deux.

    D'autre part :

    • h devant d ou u indique que l'argument est un short
    • l devant d, u, x ou X indique que l'argument est de type long
    • L devant f, e, E ou g indique que l'argument est de type long double

    La spécification de format dans printf va cependant beaucoup plus loin qu'une simple spécification de type. En règle générale, une spécification de format a la structure suivante : %[options]specificateur_de_type. Les "options" ont été mis entre crochets car elles sont toujours facultatives. Les options disponibles dépendent du code de conversion (le "spécificateur de type") utilisé. Nous n'allons pas toutes les présenter (la documentation de référence est là pour ça) mais allons quand même voir quelques options que nous aurons assez souvent l'occasion d'utiliser.

    Enfin, pour afficher un caractère, il vaut mieux utiliser putchar (ex : putchar('*')) qui est plus simple plutôt que printf.

    III-A-3-b. Les "options"

    Une spécification de format dans printf a le plus souvent (cas pratique) la structure suivante :

%[[+][-][0]][gabarit][.precision][modificateur_de_type]specificateur_de_type

    Les drapeaux + et 0 n'ont de sens que pour les données à imprimer sous forme numérique.

    Découvrons le rôle des paramètres les plus utiles pour les types les plus utilisés.

    III-A-3-c. Le type entier

    Pour le type entier (code de conversion d, u, x ou X) :

    • h devant le spécificateur de type indique que l'argument attendu est de type short et l indique que l'argument est de type long.
    • La précision indique le nombre maximum de caractères à utiliser pour imprimer la donnée.
    • Le gabarit indique le nombre minimum de caractères à utiliser.
    • Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.
    • Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.
    • Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

    Voici un programme proposant quelques applications :

    #include <stdio.h>
int main()
{
    int n = 1234;
    printf("+------------+\n");
    printf("|%12d|\n", n); /* 12 caracteres minimum, alignement a droite */
    printf("+------------+\n");
    printf("|%-12d|\n", n); /* 12 caracteres minimum, alignement a gauche */
    printf("+------------+\n");
    printf("|%012d|\n", n); /* 12 caracteres minimum, caractere de remplissage : '0' */
    printf("+------------+\n");
    /* Affichage obligatoire du signe */
    printf("|%+12d|\n", n);
    printf("+------------+\n");
    printf("|%+-12d|\n", n);
    printf("+------------+\n");
    printf("|%+012d|\n", n);
    printf("+------------+\n");
    return 0;
}

    Voici la sortie de ce programme :

    +------------+
|        1234|
+------------+
|1234        |
+------------+
|000000001234|
+------------+
|       +1234|
+------------+
|+1234       |
+------------+
|+00000001234|
+------------+
    III-A-3-d. Le type flottant

    Pour le type flottant (code de conversion f, e, E, g ou G) :

    • L devant le spécificateur de type indique que l'argument attendu est de type long double.
    • La précision indique le nombre de caractères à afficher après le point décimal pour les codes f, e et E (la valeur par défaut est 6) et le nombre maximum de chiffres significatifs pour les codes g et G.
    • Le gabarit indique le nombre minimum de caractères à utiliser.
    • Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.
    • Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.
    • Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

    Voici un programme proposant quelques applications :

    #include <stdio.h>
int main()
{
    double x = 12.34;
    /* Affichage de flottant avec 4 chiffres apres le point decimal. */
    printf("%.4f\n", x);
    printf("%.4e\n", x);
    /* Affichage dans le format le plus approprie. */
    printf("%g\n", x);
    return 0;
}

    Voici la sortie de ce programme (en supposant que l'implémentation utilise 3 chiffres pour imprimer l'exposant) :

    12.3400
1.2340e+001
12.34

    III-A-4. Les variables et les constantes

    Si vous avez déjà fait un peu de math dans votre vie, vous devriez connaître ce que c'est qu'une variable. La notion est pratiquement la même en programmation. La déclaration d'une variable en langage C, dans sa forme la plus simple, se fait de la manière suivante :

    <type> variable;

    Par exemple :

    int n;

    Voici d'autres exemples beaucoup plus généralistes :

    int a, b, c;
int i = 0, j, n = 10;
double x, y, z = 0.5;

    Une déclaration de variable de la forme :

    int n = 0;

    est appelée une déclaration avec initialisation. Ce n'est pas la même chose que :

    int n;
n = 0;

    qui est une déclaration suivie d'une instruction d'affectation. Pour vous en convaincre rapidement, une variable constante (ou varaible en lecture seule) est une variable qui ne peut être qu'initialisée puis lue (la valeur d'une constante ne peut pas être modifiée). On obtient une constante en ajoutant le mot-clé const devant une déclaration. Par exemple :

    const int n = 10;

    On ne pourra jamais écrire :

    n = <quoi que ce soit>;

    Le mot-clé const être placé avant ou après le type ou encore avant le nom de la variable. Voici deux exemples :

    const int n = 10;
int const m = 20;

    III-A-5. Définition de nouveaux types

    Le C dispose d'un mécanisme très puissant permettant au programmeur de créer de nouveaux types de données en utilisant le mot clé typedef. Par exemple :

    typedef int ENTIER;

    Définit le type ENTIER comme n'étant autre que le type int. Rien ne nous empêche donc désormais d'écrire par exemple :

    ENTIER a, b;

    Bien que dans ce cas, un simple #define aurait pu suffire, il est toujours recommandé d'utiliser typedef qui est beaucoup plus sûr.

    III-B. Les pointeurs

    III-B-1. Définition

    Comme nous le savons très bien, l'endroit où s'exécute un programme est la mémoire donc toutes les données du programme (les variables, les fonctions, ...) se trouvent en mémoire. Le langage C dispose d'un opérateur - & - permettant de récupérer l'adresse en mémoire d'une variable ou d'une fonction quelconque. Par exemple, si n est une variable, &n désigne l'adresse de n.

    Le C dispose également d'un opérateur - * - permettant d'accéder au contenu de la mémoire dont l'adresse est donnée. Par exemple, supposons qu'on ait :

    int n;

    Alors les instructions suivantes sont strictement identiques.

    n = 10;

    *( &n ) = 10;

    Un pointeur (ou une variable de type pointeur) est une variable destinée à recevoir une adresse. On dit alors qu'elle pointe sur un emplacement mémoire. L'accès au contenu de la mémoire se fait par l'intermédiaire de l'opérateur *.

    Les pointeurs en langage C sont typés et obéissent à l'arithmétique des pointeurs que nous verrons un peu plus loin. Supposons que l'on veuille créer une variable - p - destinée à recevoir l'adresse d'une variable de type int. p s'utilisera alors de la façon suivante :

    p = &n;
...
*p = 5;
...

    *p est un int. Vous suivez ? Alors comment devrait-on déclarer le pointeur ? Si vous avez bien suivi, vous auriez répondu :

    int *p;

    Et de plus, si vous avez très bien suivi, vous auriez certainement ajouté : et cela est strictement équivalent à :

    int * p;

    Car en C, les espaces sont totalement gratuites ! Donc voilà, le type d'un pointeur sur int est int *. Mais cela peut être moins évident dans certaines déclarations. Par exemple, dans :

    /* cas 1 */
int * p1, p2, p3;

    seul p1 est de type int * ! Le reste, p2 et p3, sont de type int. Si vous voulez obtenir 3 pointeurs, vous devrez utilisez la syntaxe suivante :

    /* cas 2 */
int *p1, *p2, *p3;

    Ou avec un typedef :

    typedef int * PINT;
PINT p1, p2, p3;

    Par contre cela n'aurait pas marché si on avait défini PINT à l'aide d'un #define car cela nous amènerait au cas 1.

    III-B-2. Saisir des données tapées au clavier avec la fonction scanf

    On va encore faire un petit exercice de logique, plutôt de bon sens.

    Si nous voulons afficher un entier par exemple, que doit-on fournir à la fonction d'affichage (printf par exemple) : la valeur de l'entier que nous voulons afficher ou l'adresse de la variable contenant le nombre que nous voulons afficher ?

    La bonne réponse est : l'entier que nous voulons afficher bien sûr, autrement dit sa valeur. Personne n'a demandé son adresse !

    Maintenant, si nous voulons demander à l'utilisateur (celui qui utilise notre programme) de taper un nombre puis ranger le nombre ainsi tapé dans une variable, que devons-nous fournir à la fonction permettant la saisie : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre que l'utilisateur a entré, ou la valeur que contient actuellement cette variable ?

    La bonne réponse est bien sûr : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre entré

    La fonction scanf, déclarée dans le fichier stdio.h, permet de lire des données formatées sur l'entrée standard, par défaut le clavier.

    Voici un programme qui demande à l'utilisateur d'entrer 2 nombres puis affiche leur somme :

    #include <stdio.h>
int main()
{
    int a, b, c;
    printf("Ce programme calcule la somme de 2 nombres.\n");
    printf("Entrez la valeur de a : ");
    scanf("%d", &a);
    printf("Entrez la valeur de b : ");
    scanf("%d", &b);
    c = a + b;
    printf("%d + %d = %d\n", a, b, c);
    return 0;
}

    Mais faites gaffe avec la fonction scanf ! "%d" par exemple n'a rien à voir avec " %d ". En effet dans ce dernier cas, le format attendu par la fonction est :

    <un espace> <un entier> <puis un autre espace>

    Où espace désigne en fait n'importe quel caractère ou série de caractères blancs (espace, tabulation, etc.). Voici un exemple beaucoup plus convaincant :

    #include <stdio.h>
int main()
{
    int mon_age = 23, votre_age;
    printf("Bonjour, j'ai %d ans. Et vous? ", mon_age);
    scanf("%d ans", &votre_age);
    printf("Ah! vous avez %d ans?\n", votre_age);
    return 0;
}

    L'utilisateur devra alors taper par exemple :

    22 ans

    Peu importe le nombre de caractères blancs entre 22 et ans.

    C'est ce qu'on appelle une saisie formatée. scanf retourne le nombre de conversions de format réussies. La valeur de retour de scanf permet donc déjà de voir si l'utilisateur a bien respecté le format attendu ou non. Dans notre exemple, il n'y a qu'une seule conversion de format à faire (%d). Si l'utilisateur respecte bien le format, scanf retournera 1.

    Les fonctions telles que scanf sont plutôt destinées à être utilisées pour lire des données provenant d'un programme sûr (par l'intermédiaire d'un fichier par exemple), pas celles provenant d'un humain, qui sont sujettes à l'erreur. Les codes format utilisés dans scanf sont à peu près les mêmes que dans printf, sauf pour les flottants notamment.

    Code format | Utilisation
f, e, g | float
lf, le, lg | double
Lf, Le, Lg | long double

    Voici un programme qui permet de calculer le volume d'un cône droit à base circulaire selon la formule : V = 1/3 * (B * h) où B est la surface de base soit pour une base circulaire : B = PI*R2, où R est le rayon de la base.

    #include <stdio.h>
double Volume(double r_base, double hauteur);
int main()
{
    double R, h, V;
    printf("Ce programme calcule le volume d'un cone.\n");
    printf("Entrez le rayon de la base : ");
    scanf("%lf", &R);
    printf("Entrez la hauteur du cone : ");
    scanf("%lf", &h);
    V = Volume(R, h);
    printf("Le volume du cone est : %f", V);
    return 0;
}
double Volume(double r_base, double hauteur)
{
    return (3.14 * r_base * r_base * hauteur) / 3;
}

    III-B-3. Exemple de permutation des contenus de deux variables

    Voici un programme simple qui permute le contenu de deux variables.

    include <stdio.h>
int main()
{
    int a = 10, b = 20, c;
    /* on sauvegarde quelque part le contenu de a */
    c = a;
    /* on met le contenu de b dans a */
    a = b;
    /* puis le contenu de a que nous avons sauvegardé dans c dans b */
    b = c;
    /* affichons maintenant a et b */
    printf("a = %d\nb = %d\n", a, b);
    return 0;
}

    Maintenant, réécrivons le même programme en utilisant une fonction. Cette fonction doit donc pouvoir localiser les variables en mémoire autrement dit nous devons passer à cette fonction les adresses des variables dont on veut permuter le contenu.

    #include <stdio.h>
void permuter(int * addr_a, int * addr_b);
int main()
{
    int a = 10, b = 20;
    permuter(&a, &b);
    printf("a = %d\nb = %d\n", a, b);
    return 0;
}
void permuter(int * addr_a , int * addr_b)
/***************\
* addr_a <-- &a *
* addr_b <-- &b *
\***************/
{
    int c;
    c = *addr_a;
    *addr_a = *addr_b;
    *addr_b = c;
}

    III-C. Les expressions

    III-C-1. Introduction

    En langage C, la notion d'expression est beaucoup plus riche que dans n'importe quel autre langage. De ce fait en donner une définition est longue et nous allons donc plutôt l'introduire (comme nous l'avons d'ailleurs toujours fait) de manière intuitive. Pour cela, analysons l'instruction suivante :

    c = a + b;

    Dans cette instruction (où nous supposons que les 3 variables sont tous de type int), il y a tout d'abord évaluation de a + b, puis affection du résultat dans c. On dit que :

    a + b

    est une expression, plus précisément une expression de type int (car a et b sont de type int) et dont la valeur est la somme de a et b (a + b), et que :

    c = a + b;

    est une instruction (remarquez bien le point-virgule).

    L'expression a + b est composée de deux expressions à savoir a et b. On dit alors que c'est une expression complexe. En langage C, les opérateurs permettent de construire des expressions complexes. Dans notre exemple, l'opérateur d'addition à savoir + nous a permis de construire une expression complexe, a + b, à partir des expressions simples a et b.

    Or en langage C, = est également un opérateur, l'opérateur d'affectation, et permet donc de construire des expressions complexes. Ainsi, dans notre exemple :

    c = a + b

    est une expression (remarquez bien l'absence de point-virgule, sinon ce serait une instruction) et dont la valeur et le type sont celle et celui de l'expression situé à gauche à savoir c.

    III-C-2. lvalue et rvalue

    Une lvalue (de left value) est tout ce qui peut figurer à gauche de l'opérateur d'affectation. Une variable, par exemple, est une lvalue. Si p est un pointeur (disons vers int), p est une lvalue (en effet un pointeur est une variable comme toutes les autres) et *p est également une lvalue.

    Une rvalue (de right value) est tout ce qui peut figurer à droite de l'opérateur d'affectation. Une variable, une constante littérale, l'adresse d'une variable en sont des exemples. Si a est une variable, disons de type int, alors a est une lvalue tandis que a, -a ou a + 1 par exemple sont des rvalues. Cet exemple nous montre bien qu'une variable est à la fois une lvalue et une rvalue.

    Une rvalue possède une valeur mais pas une adresse. Ainsi par exemple : a étant une lvalue, &a a bien un sens (désigne l'adresse mémoire de la variable a). Par contre, une expression insensée telle que &(-a) ou &(a + 1) sera rejetée (avec plaisir) par le compilateur.

    III-C-3. Opérations usuelles

    III-C-3-a. Les opérateurs arithmétiques courants

    Les opérateurs arithmétiques courants +, -, * et / existent en langage C. Toutefois, la division entière est un tout petit peu délicate. En effet, si a et b sont des entiers, a / b vaut le quotient de a et b c'est-à-dire par exemple, 29 / 5 vaut 5. Le reste d'une division entière s'obtient avec l'opérateur modulo %, c'est-à-dire, en reprenant l'exemple précédent, 29 % 5 vaut 4.

    III-C-3-b. Les opérateurs de comparaison

    Les opérateurs de comparaison sont

    Opérateur | Rôle
< | Inférieur à
> | Supérieur à
== | Egal à
<= | Inférieur ou égal à
>= | Supérieur ou égal à
!= | Différent de

    Par exemple, l'expression :

    1 < 1000

    est vraie. Vraie ? Et que vaut « vrai » au juste ? Quel est le type d'une expression « vraie » ? En langage C, la valeur d'une expression « vraie » est non nulle et celle d'une expression « fausse » zéro. Et réciproquement, toute valeur non nulle (24, -107, 2.9, ...) peut être interprétée comme vrai et zéro comme faux. Par exemple :

    int prop;
prop = (1 < 1000); /* alors prop = VRAI */
    III-C-3-c. Les opérateurs logiques

    On peut construire des expressions logiques complexes à l'aide des opérateurs

    Opérateur | Rôle
&& | ET
|| | OU
! | NON

    Par exemple :

    int prop1, prop2, prop_ou, prop_et, prop_vrai;
prop1 = (1 < 1000);
prop2 = (2 == -6);
prop_ou = prop1 || prop2; /* VRAI, car prop1 est VRAI */
prop_et = prop1 && prop2; /* FAUX, car prop2 est FAUX */
prop_vrai = prop1 && !prop_2 /* VRAI car prop1 et !prop2 sont VRAI */

    Les opérateurs logiques jouissent des propriétés suivantes :

    • Dans une opération ET, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est fausse, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours FAUX.

    • Dans une opération OU, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est vrai, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours VRAI.

    III-C-4. L'opérateur virgule

    On peut séparer plusieurs expressions à l'aide de l'opérateur virgule. Le résultat est une expression dont la valeur est celle de l'expression la plus à droite. L'expression est évaluée de gauche à droite. Par exemple, l'expression :

    (a = -5, b = 12, c = a + b) * 2

    vaut 14.

    III-C-5. Taille des données. L'opérateur sizeof

    La taille d'une donnée désigne la taille, en octets, que celle-ci occupe en mémoire. Par extension de cette définition, la taille d'un type de données désigne la taille d'une donnée de ce type. Attention ! octet désigne ici, par abus de langage, la taille d'un élément de mémoire sur la machine cible (la machine abstraite), c'est-à-dire la taille d'une case mémoire (qui vaut 8 bits dans la plupart des architectures actuelles), et non un groupe de 8 bits. En langage C, un octet (une case mémoire) est représenté par un char. La taille d'un char n'est donc pas forcément 8 bits, même si c'est le cas dans de nombreuses architectures, mais dépendante de la machine. La norme requiert toutefois qu'un char doit faire au moins 8 bits et que la macro CHAR_BIT, déclarée dans limits.h, indique la taille exacte d'un char sur la machine cible.

    Le C dispose d'un opérateur, sizeof, permettant de connaître la taille, en octets, d'une donnée ou d'un type de données. La taille d'un char vaut donc évidemment 1 puisqu'un char représente un octet. Par ailleurs, il ne peut y avoir de type dont la taille n'est pas multiple de celle d'un char. Le type de la valeur retournée par l'opérateur sizeof est size_t, déclaré dans stddef.h, qui est inclus par de nombreux fichiers d'en-tête dont stdio.h.

    Comme nous l'avons déjà dit plus haut, la taille des données est dépendante de la machine cible. En langage C, la taille des données n'est donc pas fixée. Néanmoins la norme stipule qu'on doit avoir :

    sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeofd(long)

    Sur un processeur Intel (x86) 32 bits par exemple, un char fait 8 bits, un short 16 bits, et les int et les long 32 bits.

    III-C-6. Les opérateurs d'incrémentation et de décrémentation

    Il s'agit des opérateurs ++ (opérateur d'incrémentation) et -- (opérateur de décrémentation). Ils existent sous forme préfixée ou postfixée. L'opérande doit être une lvalue de type numérique ou pointeur. Soient i et j deux variables de type int :

    j = i++;

    est équivalent à :

    j = i;
i = i + 1;

    On voit donc bien que la valeur de i++ est celle de i avant l'évaluation effective de l'expression (post incrémentation).

    Inversement :

    j = ++i;

    est équivalent à :

    i = i + 1;
j = i;

    Où l'on voit bien que la valeur de ++i est celle de i après l'évaluation effective de l'expression (pré incrémentation).

    L'opérateur -- s'utilise de la même manière que ++ mais l'opérande est cette fois-ci décrémentée.

    III-C-7. Expressions conditionnelles

    Une expression conditionnelle est une expression dont la valeur dépend d'une condition. L'expression :

    p ? a : b

    vaut a si p est vrai et b si p est faux.

    III-C-8. Autres opérateurs d'affectation

    Ce sont les opérateurs : +=, -=, *=, /=, ...

    x += a;

    par exemple est équivalent à :

    x = x + a;

    III-C-9. Ordre de priorité des opérateurs

    Les opérateurs sont classés par ordre de priorité. Voici les opérateurs que nous avons étudiés jusqu'ici classés dans cet ordre. Opérateur Associativité

    Opérateur | Associativité
Parenthèses | de gauche à droite
! ++ -- - (signe) sizeof | de gauche à droite
* / % | de gauche à droite
+ - | de gauche à droite
< <= > >= | de gauche à droite
== != | de gauche à droite
& (adresse de) | de gauche à droite
&& | de gauche à droite
|| | de gauche à droite
Opérateurs d'affectation (= += ...) | de droite à gauche
, | de gauche à droite

    Ce n'est pas parce que cet ordre existe qu'il faut le retenir par coeur. Pour du code lisible, il est même conseillé de ne pas trop en tenir compte et d'utiliser des parenthèses dans les situations ambiguës.

    III-D. Considérations liées à la représentation binaire

    III-D-1. Généralités

    L'ordinateur traite les données uniquement sous forme numérique. A l'intérieur de l'ordinateur, ces nombres sont représentés en binaire. La manière de représenter une donnée dans la mémoire de l'ordinateur est appelée codage. Pour bien comprendre le C, vous devez connaître comment les données sont représentées en mémoire, c'est-à-dire sous leur forme binaire. Voir à ce sujet votre cours de numérique.

    III-D-2. Les caractères

    La représentation numérique des caractères définit ce qu'on appelle un jeu de caractères. Par exemple, dans le jeu de caractères ASCII (American Standard Code for Information Interchange) qui est un jeu de caractères qui n'utilise que 7 bits et qui est à la base de nombreux codes populaires de nos jours, le caractère 'A' est représenté par le code 65, le caractère 'a' par 97 et '0' par 48. Hélas, même ASCII n'est pas adopté par le langage C. En effet si le C dépendait d'un jeu de caractères particulier, il ne serait alors pas totalement portable. Le standard définit néanmoins un certain nombre de caractères que tout environnement compatible avec le C doit posséder parmi lesquels les 26 lettres de l'alphabet latin (donc en fait 52 puisqu'on différencie les majuscules et les minuscules), les 10 chiffres décimaux, les caractères # < > ( ) etc. Le programmeur (mais pas le compilateur) n'a pas besoin de connaître comment ces caractères sont représentés dans le jeu de caractères de l'environnement. Le standard ne définit donc pas un jeu de caractères mais seulement un ensemble de caractères que chaque environnement compatible est libre d'implémenter à sa façon (plus éventuellement les caractères spécifiques à cet environnement). La seule contrainte imposée est que leur valeur doit pouvoir tenir dans un char.

    Concernant la technique d'échappement, sachez également qu'on peut insérer du code octal (commençant par 0) ou hexadécimal (commençant par x) après le caractère d'échappement \ pour obtenir un caractère dont le code dans le jeu de caractères est donné. L'hexadécimal est de loin le plus utilisé. Par exemple : '\x30', '\x41', '\x61', ... Et enfin pour les caractères de code 0, 1, ... jusqu'à 7, on peut utiliser les raccourcis '\0', '\1', ... '\7'.

    III-D-3. Dépassement de capacité

    Le dépassement de capacité a lieu lorsqu'on tente d'affecter à une lvalue une valeur plus grande que ce qu'elle peut contenir. Par exemple, en affectant une valeur sur 32 bits à une variable ne pouvant contenir que 16 bits.

    III-E. La conversion de type

    III-E-1. Conversion implicite

    En langage C, des règles de conversion dite implicite s'appliquent aux données qui composent une expression complexe lorsqu'ils ne sont pas de même type (entier avec un flottant, entier court avec entier long, entier signé avec un entier non signé, etc.). Par exemple, dans l'expression :

    'A' + 2

    'A' est de type char et 2 de type int. Dans ce cas, 'A' est tout d'abord converti en int avant que l'expression ne soit évaluée. Le résultat de l'opération est de type int (car un int + un int donne un int). Ici, il vaut 67 (65 + 2). En fait, les char et les short sont toujours systématiquement convertis en int c'est-à-dire que dans l'addition de deux char par exemple, tous deux sont tout d'abord convertis en int avant d'être additionnés, et le résultat est un int (pas un char). Un unsigned char sera converti en unsigned int, et ainsi de suite.

    En règle générale : le type le plus « faible » est convertit dans le type le plus « fort ». Par exemple, les entiers sont plus faibles que les flottants donc 1 mélangé à un flottant par exemple sera tout d'abord converti en 1.0 avant que l'opération n'ait effectivement lieu.

    III-E-2. Conversion explicite (cast)

    Il suffit de préciser le type de destination entre parenthèses devant l'expression à convertir. Par exemple :

    float f;
f = (float)3.1416;

    Dans cet exemple, on a converti explicitement 3.1416, qui est de type double, en float. Lorsqu'on affecte un flottant à un entier, seule la partie entière, si elle peut être représentée, est retenue.

    III-F. Les instructions

    III-F-1. Introduction

    L'instruction est la plus petite unité qui compose une fonction. Le seul type d'instruction que nous avons vu jusqu'ici est l'expression-instruction qui n'est autre qu'une expression suivie d'un point-virgule. Comprendre les notions d'expression et d'instruction est indispensable pour bien comprendre et maîtriser la syntaxe du langage C. Nous allons maintenant généraliser la notion d'instruction en langage C.

    III-F-2. Bloc d'instructions

    Un bloc, abusivement appelé bloc d'instructions, ou plus précisément une instruction-bloc consiste en un groupe de déclarations et d'instructions (qui peuvent être également des blocs). Un bloc est délimité par des accolades. Une fonction est toujours constituée d'une instruction-bloc.

    Une variable déclarée à l'intérieur d'un bloc est locale à (c'est-à-dire n'est connue qu'à l'intérieur de) ce bloc et masque toute variable de même nom déclarée à l'extérieur de celui-ci (une variable globale est une variable déclarée à l'extérieur de toute fonction et est connue partout dans le fichier source). D'autre part, une déclaration doit toujours se faire en début de bloc. Les arguments effectifs d'une fonction sont également des variables locales à cette fonction.

    Voici un exemple de programme (présentant peu d'intérêt je le conçois) comportant plusieurs blocs :

    #include <stdio.h>
int main()
{
    printf("Les instructions en langage C\n");
    printf("-----------------------------\n\n");
    {
        /* Un bloc d'instructions */
        printf("**********\n");
        printf("* Bloc 1 *\n");
        printf("**********\n");
    }
    putchar('\n');
    {
        /* Un autre bloc */
        printf("**********\n");
        printf("* Bloc 2 *\n");
        printf("**********\n");
        {
            /* Encore un autre bloc */
            printf("**********\n");
            printf("* Bloc 3 *\n");
            printf("**********\n");
        }
    }
    return 0;
}

    Retenez donc bien ceci : un bloc constitue bien une et une seule instruction. Bien que cette instruction soit en fait un bloc d'instructions (et de déclarations), il est conseillé de la voir comme une et une seule instruction plutôt que comme un bloc d'instructions. Une instruction-bloc ne se termine pas par un point-virgule. Conclusion : Il est faux de dire qu'une instruction doit toujours se terminer par un point-virgule. Le point-virgule marque tout simplement la fin de certaines instructions, dont les expressions-instructions.

    III-F-3. L'instruction if

    Permet d'effectuer des choix conditionnels. La syntaxe de l'instruction est la suivante :

    if ( <expression> )
    <une et une seule instruction>
else
    <une et une seule instruction>

    Une instruction if peut ne pas comporter de else. Lorsqu'on a plusieurs instructions if imbriquées, un else se rapporte toujours au dernier if suivi d'une et une seule instruction. Par exemple : écrivons un programme qui compare deux nombres.

    #include <stdio.h>
int main()
{
    int a, b;
    printf("Ce programme compare deux nombres.\n");
    printf("Entrez la valeur de a : ");
    scanf("%d", &a);
    printf("Entrez la valeur de b : ");
    scanf("%d", &b);
    if (a < b)
        printf("a est plus petit que b.\n");
    else
        if (a > b)
            printf("a est plus grand que b.\n");
        else
            printf("a est egal a b.\n");
    return 0;
}

    III-F-4. L'instruction do

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    do
    <une et une seule instruction>
while ( <expression> );

    L'instruction do permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait après chaque exécution de l'instruction. Voici un programme qui affiche 10 fois Bonjour.

    #include <stdio.h>
int main()
{
    int nb_lignes_affichees = 0;
    do
        {
            printf("Bonjour.\n");
            nb_lignes_affichees++;
        }
    while (nb_lignes_affichees < 10);
    return 0;
}

    III-F-5. L'instruction while

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    while ( <expression> )
    <une et une seule instruction>

    L'instruction while permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait avant chaque exécution de l'instruction. Donc si la condition (<expression>) est fausse dès le départ, la boucle ne sera pas exécutée.

    III-F-6. L'instruction for

    Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

    for ( <init> ; <condition> ; <step>)
    <instruction>

    Elle est pratiquement identique à :

    <init>;
while ( <condition> )
{
    <instruction>
    <step>
}

    Par exemple, écrivons un programme qui affiche la table de multiplication par 5.

    #include <stdio.h>
int main()
{
    int n;
    for(n = 0; n <= 10; n++)
        printf("5 x %2d %2d\n", n, 5 * n);
    return 0;
}

    III-F-7. Les instructions switch et case

    Ces instructions permettent d'éviter des instructions if trop imbriquées comme illustré par l'exemple suivant :

    #include <stdio.h>
int main()
{
    int n;
    printf("Entrez un nombre entier : ");
    scanf("%d", &n);
    switch(n)
    {
    case 0:
        printf("Cas de 0.\n");
        break;
    case 1:
        printf("Cas de 1.\n");
        break;
    case 2: case 3:
        printf("Cas de 2 ou 3.\n");
        break;
    case 4:
        printf("Cas de 4.\n");
        break;
    default:
        printf("Cas inconnu.\n");
    }
    return 0;
}

    III-F-8. L'instruction break

    Permet de sortir immédiatement d'une boucle ou d'un switch. La syntaxe de cette instruction est :

    break;

    III-F-9. L'instruction continue

    Dans une boucle, permet de passer immédiatement à l'itération suivante. Par exemple, modifions le programme table de multiplication de telle sorte qu'on affiche rien pour n = 4 ou n = 6.

    #include <stdio.h>
int main()
{
    int n;
    for(n = 0; n <= 10; n++)
    {
        if ((n == 4) || (n == 6))
            continue;
        printf("5 x %2d %2d\n", n, 5 * n);
    }
    return 0;
}

    III-F-10. L'instruction return

    Permet de terminer une fonction. La syntaxe de cette instruction est la suivante :

    return <expression>; /* termine la fonction et retourne <expression> */

    ou :

    return; /* termine la fonction sans spécifier de valeur de retour */

    III-F-11. L'instruction vide

    Une instruction peut être vide. Par exemple :

    /* Voici une instruction vide * / ;
/* Voici une autre instruction vide */ ;
{ /* Encore une instruction vide */ }

    III-G. Exercices

    III-G-1. Valeur absolue

    Ecrire un programme qui demande à l'utilisateur d'entrer un nombre et qui affiche ensuite la "valeur absolue" de ce nombre. Il y a plusieurs façons de définir la valeur absolue d'un nombre x mais nous utiliserons la suivante : c'est x si x est supérieur ou égal à 0 et -x si x est inférieur à 0. La valeur absolue d'un nombre est donc un nombre toujours positif ou nul. Voici un exemple d'exécution :

    Ce programme permet de determiner la valeur absolue d'un nombre.
Entrez un nombre : -6
La valeur absolue de -6 est 6.
Merci d'avoir utilise ce programme. A bientot !


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

Table des matières

  • IV. Tableaux, pointeurs et chaînes de caractères
    • IV-A. Les tableaux
      • IV-A-1. Définition
      • IV-A-2. Initialisation
      • IV-A-3. Création d'un type « tableau »
      • IV-A-4. Les tableaux à plusieurs dimensions
      • IV-A-5. Calculer le nombre d'éléments d'un tableau
    • IV-B. Les pointeurs
      • IV-B-1. Les tableaux et les pointeurs
      • IV-B-2. L'arithmétique des pointeurs
      • IV-B-3. Pointeurs constants et pointeurs sur constante
      • IV-B-4. Pointeurs génériques
      • IV-B-5. Exemple avec un tableau à plusieurs dimensions
      • IV-B-6. Passage d'un tableau en argument d'une fonction
    • IV-C. Les chaînes de caractères
      • IV-C-1. Chaîne de caractères
      • IV-C-2. Longueur d'une chaîne
      • IV-C-3. Représentation des chaînes de caractères en langage C
      • IV-C-4. Les fonctions de manipulation de chaîne
        • strcpy, strncpy
        • strcat, strncat
        • strlen
        • strcmp, strncmp
      • IV-C-5. Fusion de chaînes littérales
    • IV-D. Exercices
      • IV-D-1. Recherche dans un tableau
      • IV-D-2. Calcul de la moyenne
      • IV-D-3. Manipulation de chaînes

    • IV. Tableaux, pointeurs et chaînes de caractères

    IV-A. Les tableaux

    IV-A-1. Définition

    Un tableau est un regroupement d'une ou plusieurs données de même type contigus en mémoire. L'accès à un élément du tableau se fait par un système d'indice, l'indice du premier élément étant 0. Par exemple :

    int t[10];

    déclare un tableau de 10 éléments (de type int) dont le nom est t. Les éléments du tableau vont donc de t[0], t[1], t[2] ... à t[9]. t est une variable de type tableau, plus précisément (dans notre cas), une variable de type tableau de 10 int (int [10]). Les éléments du tableau sont des int. Ils peuvent être utilisés comme n'importe quelle variable de type int.

    IV-A-2. Initialisation

    On peut initialiser un tableau à l'aide des accolades. Par exemple :

    int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

    Bien évidemment, on n'est pas obligé d'initialiser tous les éléments, on aurait donc pu par exemple nous arrêter après le 5ème élément, et dans ce cas les autres éléments du tableau seront automatiquement initialisés à 0. Attention ! une variable locale non initialisée contient « n'importe quoi », pas 0 !

    Lorsqu'on déclare un tableau avec initialisation, on peut ne pas spécifier le nombre d'éléments car le compilateur le calculera automatiquement. Ainsi, la déclaration :

    int t[] = {0, 10, 20, 30};

    est strictement identique à :

    int t[4] = {0, 10, 20, 30};

    IV-A-3. Création d'un type « tableau »

    Tout d'abord, étudions un peu la logique du mot-clé typedef. Pour cela, supposons que l'on veuille pouvoir utiliser indifféremment les formules suivantes pour déclarer un entier :

    int x;

    et

    ENTIER x;

    Dans la première forme, on remplace x par ENTIER et on la fait précéder du mot-clé typedef, ce qui nous donne :

    typedef int ENTIER;

    Concrètement, cela signifie : un ENTIER est tout ce qui suit le mot int dans une déclaration. On peut également formuler une phrase similaire (genre : un TABLEAU est tout ce qui est précédé du mot int et suivi de [10] dans sa déclaration) pour définir un type TABLEAU mais il y a plus malin :

    On remplace t par TABLEAU dans sa déclaration puis on ajoute devant le mot-clé typedef, exactement comme ce qu'on a fait avec x et ENTIER. Ainsi, le type tableau de 10 int se définit de la manière suivante :

    typedef int TABLEAU[10];

    Désormais :

    int t[10];

    et :

    TABLEAU t;

    sont strictement équivalents.

    IV-A-4. Les tableaux à plusieurs dimensions

    On peut également créer un tableau à plusieurs dimensions. Par exemple :

    int t[10][3];

    Un tableau à plusieurs dimensions n'est en fait rien d'autre qu'un tableau (tableau à une dimension) dont les éléments sont des tableaux. Comme dans le cas des tableaux à une dimension, le type des éléments du tableau doit être parfaitement connu. Ainsi dans notre exemple, t est un tableau de 10 tableaux de 3 int, ou pour vous aider à y voir plus clair :

    typedef int TRIPLET[3];
TRIPLET t[10];

    Les éléments de t vont de t[0] à t[9], chacun étant un tableau de 3 int.

    On peut bien entendu créer des tableaux à 3 dimensions, 4, 5, 6, ...

    On peut également initialiser un tableau à plusieurs dimensions. Par exemple :

    int t[3][4] = { {0, 1, 2, 3},
                {4, 5, 6, 7},
                {8, 9, 10, 11} };

    Qu'on aurait également pu tout simplement écrire :

    int t[][4] = { {0, 1, 2, 3},
               {4, 5, 6, 7},
               {8, 9, 10, 11} };

    IV-A-5. Calculer le nombre d'éléments d'un tableau

    La taille d'un tableau est évidemment le nombre d'éléments du tableau multiplié par la taille de chaque élément. Ainsi, le nombre d'éléments dans un tableau est égal à sa taille divisée par la taille d'un élément. On utilise alors généralement la formule sizeof(t) / sizeof(t[0]) pour connaître le nombre d'éléments d'un tableau t. La macro définie ci-dessous permet de calculer la taille d'un tableau :

    #define COUNT(t) (sizeof(t) / sizeof(t[0]))

    IV-B. Les pointeurs

    IV-B-1. Les tableaux et les pointeurs

    Pour nous fixer les idées, considérons le tableau t suivant :

    char t[10];

    Mais les règles que nous allons établir ici s'appliquent à n'importe quel type de tableau, y compris les tableaux à plusieurs dimensions.

    Définissons ensuite le type TABLEAU par :

    typedef char TABLEAU[10];

    Mais avant d'aller plus loin, j'aimerais déjà préciser que les tableaux et les pointeurs n'ont rien de commun, à part peut-être le fait qu'on peut pointer sur n'importe quel élément d'un tableau (ou sur un tableau ...), tout comme on peut pointer sur n'importe quoi. Mais il y a quand même une chose qui lie les deux notions, c'est que : lorsqu'elle n'est pas utilisée en unique argument de sizeof ou en unique argument de l'opérateur & ("adresse de"), une expression de type tableau est toujours convertie par le compilateur en l'adresse de son premier élément. Cela signifie, dans ces conditions, que si t est un tableau de disons 10 éléments, l'écriture t est strictement équivalente à &t[0], donc t + 1 (qui est strictement équivalent à &(t[0]) + 1) est strictement équivalent à &t[1] et ainsi de suite. Ainsi :

    t[5] = '*';

    est strictement équivalent à :

    *(t + 5) = '*';

    Et ainsi de suite.

    Dans la pratique, on utilise un pointeur sur un élément du tableau, généralement le premier. Cela permet d'accéder à n'importe quel élément du tableau par simple calcul d'adresse. En effet, comme nous l'avons dit plus haut : t + 1 est équivalent à &(t[1]), t + 2 à &(t[2]), etc.

    Voici un exemple qui montre une manière de parcourir un tableau :

    #include <stdio.h>
#define COUNT(t) (sizeof(t) / sizeof(t[0]))
void Affiche(int * p, size_t nbElements);
int main()
{
    int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
    Affiche(t, COUNT(t));
    return 0;
}
void Affiche(int * p, size_t nbElements)
{
    size_t i;

    for(i = 0; i < nbElements; i++)
        printf("%d\n", p[i]);
}

    IV-B-2. L'arithmétique des pointeurs

    L'arithmétique des pointeurs est née des faits que nous avons établis précédemment. En effet si p pointe sur un élément d'un tableau, p + 1 doit pointer sur l'élément suivant. Donc si la taille de chaque élément du tableau est par exemple de 4, p + 1 déplace le pointeur de 4 octets (où se trouve l'élément suivant) et non de un.

    De même, puisque l'on devrait avoir (p + 1) - p = 1 et non 4, la différence entre deux adresses donne le nombre d'éléments entre ces adresses et non le nombre d'octets entre ces adresses. Le type d'une telle expression est ptrdiff_t, qui est défini dans le fichier stddef.h.

    Et enfin, l'écriture p[i] est strictement équivalente à *(p + i).

    Cela montre à quel point le typage des pointeurs est important. Cependant, il existe des pointeurs dits génériques capables de pointer sur n'importe quoi. Ainsi, la conversion d'un pointeur générique en un pointeur d'un autre type par exemple ne requiert aucun cast et vice versa.

    IV-B-3. Pointeurs constants et pointeurs sur constante

    L'utilisation du mot-clé const avec les pointeurs est au début un peu délicate. En effet, en C on a ce qu'on appelle des pointeurs constants et des pointeurs sur constante (et donc aussi des pointeurs contants sur constante ...). Il faut bien savoir les différencier.

    • Pointeur constant : Le pointeur, qui est une variable comme toute les autres, est déclaré const.
    • Pointeur sur constante : C'est l'objet pointé qui est constant.

    Dans le deuxième cas, l'objet pointé n'est pas nécessairement une constante. Seulement, on ne pourra pas le modifier via le pointeur (qui considère que l'objet est une constante). De même, on peut pointer sur une mémoire en lecture seule avec n'importe quel pointeur (et non nécessairement un pointeur sur constante) mais cela ne signifie pas que la mémoire devient désormais accessible en écriture (puisque la mémoire pointée est en lecture seule. Ce n'est pas en pointant là-dessus avec quoi que ce soit qu'on pourra changer cela.). Si le pointeur n'est pas un pointeur sur contante alors qu'il pointe sur une constante, le compilateur acceptera évidemment de modifier le contenu de la mémoire pointée (puisque l'écriture est syntaxiquement correcte), mais le problème se manifestera à l'exécution, lorsque le programme tentera de modifier le contenu de la zone en lecture seule.

    Comme int * est le type d'un pointeur sur int, un pointeur constant sur un int est de type

    int * const

    Si on avait placé le mot-clé const avant int *, on obtiendrait :

    const int *

    qui correspond plutôt, contrairement à ce qu'on attendait, au type pointeur sur const int (pointeur sur constante) ! Voici deux illustrations de l'utilisation de const avec les pointeurs :

    int n, m;
int * const p = &n;
*p = 10;
/* p = &m : INTERDIT ! p est une constante ! */

    int n, m;
int const * p = &n; /* Ou const int * p = &n; */
p = &m;
/* *p = 10 : INTERDIT ! *p est une constante ! */

    IV-B-4. Pointeurs génériques

    Le type des pointeurs génériques est void *. Comme ces pointeurs sont génériques, la taille des données pointées est inconnue et l'arithmétique des pointeurs ne s'applique donc pas à eux. De même, puisque la taille des données pointées est inconnue, l'opérateur d'indirection * ne peut être utilisé avec ces pointeurs, un cast est alors obligatoire. Par exemple :

    int n;
void * p;
p = &n;
*((int *)p) = 10; /* p etant desormais vu comme un int *, on peut alors lui appliquer l'operateur *. */

    Etant donné que la taille de toute donnée est multiple de celle d'un char, le type char * peut être également utilisé en tant que pointeur universel. En effet, une variable de type char * est un pointeur sur octet autrement dit peut pointer n'importe quoi. Cela s'avère pratique des fois (lorsqu'on veut lire le contenu d'une mémoire octet par octet par exemple) mais dans la plupart des cas, il vaut mieux toujours utiliser les pointeurs génériques. Par exemple, la conversion d'une adresse de type différent en char * et vice versa nécessite toujours un cast, ce qui n'est pas le cas avec les pointeurs génériques.

    Dans printf, le spécificateur de format %p permet d'imprimer une adresse (void *) dans le format utilisé par le système.

    Et pour terminer, il existe une macro à savoir NULL, définie dans stddef.h, permettant d'indiquer q'un pointeur ne pointe nulle part. Son intérêt est donc de permettre de tester la validité d'un pointeur et il est conseillé de toujours initialiser un pointeur à NULL.

    IV-B-5. Exemple avec un tableau à plusieurs dimensions

    Soit :

    int t[10][3];

    Définissons le type TRIPLET par :

    typedef int TRIPLET[3];

    De façon à avoir :

    TRIPLET t[10];

    En dehors du cas sizeof et opérateur & ("adresse de"), t représente l'adresse de t[0] (qui est un TRIPLET) donc l'adresse d'un TRIPLET. En faisant t + 1, on se déplace donc d'un TRIPLET soit de 3 int.

    D'autre part, t peut être vu comme un tableau de 30 int (3 * 10 = 30) puisque les éléments d'un tableau sont toujours contigus en mémoire. On peut donc accéder à n'importe quel élément de t à l'aide d'un pointeur sur int.

    Soit p un pointeur sur int et faisons :

    p = (int *)t;

    On a alors, numériquement, les équivalences suivantes :

    t | p
t + 1 | p + 3
t + 2 | p + 6
... |
t + 9 | p + 27

    Prenons alors à présent, le 3ème TRIPLET de t soit t[2].

    Puisque le premier élément de t[2] se trouve à l'adresse t + 2 soit p + 6, deuxième se trouve en p + 6 +1 et le troisième et dernier en p + 6 + 2. A près cet entier, on se retrouve au premier élément de t[3], en p + 9.

    En conclusion, pour un tableau déclaré :

    <type> t[N][M];

    on a la formule :

    t[i][j] = *(p + N*i + j) /* ou encore p[N*i + j] */

    Où évidemment : p = (int *)t.

    Et on peut bien sur étendre cette formule pour n'importe quelle dimension.

    IV-B-6. Passage d'un tableau en argument d'une fonction

    Nous avons déjà vu que le passage d'un tableau en argument d'une fonction se fait tout simplement en passant l'adresse de son premier élément. Sachez également que, en argument d'une fonction,

    <type> <identificateur>[]

    est strictement équivalent à :

    <type> * <identificateur>

    Alors que dans une « simple » déclaration, la première déclare un tableau (qui doit être impérativement initialisé) et la deuxième un pointeur. On peut également préciser le nombre d'éléments du tableau mais cette information sera complètement ignorée par le compilateur. Elle n'a donc d'intérêt que pour la documentation.

    IV-C. Les chaînes de caractères

    IV-C-1. Chaîne de caractères

    Par définition, une chaîne de caractères, ou tout simplement : chaîne, est une suite finie de caractères. Par exemple, "Bonjour", "3000", "Salut !", "EN 4", ... sont des chaînes de caractères. En langage C, une chaîne de caractères littérale s'écrit entre double quottes, exactement comme dans les exemples donnés ci-dessus.

    IV-C-2. Longueur d'une chaîne

    La longueur d'une chaîne est le nombre de caractères qu'elle comporte. Par exemple, la chaîne "Bonjour" comporte 7 caractères ('B', 'o', 'n', 'j', 'o', 'u' et 'r'). Sa longueur est donc 7. En langage C, la fonction strlen, déclarée dans le fichier string.h, permet d'obtenir la longueur d'une chaîne passée en argument. Ainsi, strlen("Bonjour") vaut 7.

    IV-C-3. Représentation des chaînes de caractères en langage C

    Comme nous l'avons déjà mentionné plus haut, les chaîne de caractères littérales s'écrivent en langage C entre double quottes. En fait, le langage C ne dispose pas vraiment de type chaîne de caractères. Une chaîne est tout simplement représentée à l'aide d'un tableau de caractères.

    Cependant, les fonctions manipulant des chaînes doivent être capables de détecter la fin d'une chaîne donnée. Autrement dit, toute chaîne de caractères doit se terminer par un caractère indiquant la fin de la chaîne. Ce caractère est le caractère '\0' et est appelé le caractère nul ou encore caractère de fin de chaîne. Son code ASCII est 0. Ainsi la chaîne "Bonjour" est en fait un tableau de caractères dont les éléments sont 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0', autrement dit un tableau de 8 caractères et on a donc "Bonjour"[0] = 'B', "Bonjour"[1] = 'o', "Bonjour"[2] = 'n', ... "Bonjour"[7] = '\0'. Toutefois, la mémoire utilisée pour stocker la chaîne peut être accessible qu'en lecture, le il n'est donc pas portable de tenter de la modifier.

    Les fonctions de manipulation de chaîne de la bibliothèque standard du langage C sont principalement déclarées dans le fichier string.h. Voici un exemple d'utilisation d'une de ces fonctions.

    #include <stdio.h>
#include <string.h>
int main()
{
    char t[50];

    strcpy(t, "Hello, world!");
    printf("%s\n", t);

    return 0;
}

    Dans cet exemple, la chaîne t ne peut contenir tout au plus que 50 caractères, caractère de fin de chaîne inclus. Autrement dit t ne peut que contenir 49 caractères « normaux » car il faut toujours réserver une place pour le caractère de fin de chaîne : '\0'. On peut aussi bien sûr initialiser une chaîne au moment de sa déclaration, par exemple :

    char s[50] = "Bonjour";

    Qui est strictement équivalente à :

    char s[50] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0'};

    Puisque, vu d'un pointeur, la valeur d'une expression littérale de type chaîne n'est autre que l'adresse de son premier élément, on peut utiliser un simple pointeur pour manipuler une chaîne. Par exemple :

    char * p = "Bonjour";

    Dans ce cas, p pointe sur le premier élément de la chaîne "Bonjour". Or, comme nous l'avons déjà dit plus haut, la mémoire allouée pour la chaîne "Bonjour" est en lecture seule donc on ne peut pas écrire par exemple :

    p[2] = '*'; /* Interdit */

    Avec un tableau, ce n'est pas l'adresse en mémoire de la chaîne qui est stockée, mais les caractères de la chaîne, copiés caractère par caractère. La mémoire utilisée par le tableau étant indépendante de celle utilisée par la chaîne source, on peut faire ce qu'on veut de notre tableau. La fonction strcpy permet de copier une chaîne vers un autre emplacement mémoire.

    Le paragraphe suivant discute des fonctions de manipulation de chaînes en langage C.

    IV-C-4. Les fonctions de manipulation de chaîne

    strcpy, strncpy
    #include <stdio.h>
#include <string.h>
int main()
{
    char t1[50], t2[50];

    strcpy(t1, "Hello, world!");
    strcpy(t2, "*************");
    strncpy(t1, t2, 3);
    printf("%s\n", t1);

    return 0;
}

    Attention ! Si t1 n'est pas assez grand pour pouvoir contenir la chaîne à copier, vous aurez un débordement de tampon (buffer overflow). Un tampon (ou buffer) est tout simplement une zone de la mémoire utilisée par un programme pour stocker temporairement des données. Par exemple, t1 est un buffer de 50 octets. Il est donc de la responsabilité du programmeur de ne pas lui passer n'importe quoi ! En effet en C, le compilateur suppose que le programmeur sait ce qu'il fait !

    La fonction strncpy s'utilise de la même manière que strcpy. Le troisième argument indique le nombre de caractères à copier. Aucun caractère de fin de chaîne n'est automatiquement ajouté.

    strcat, strncat
    #include <stdio.h>
#include <string.h>
int main()
{
    char t[50];

    strcpy(t, "Hello, world");
    strcat(t, " from");
    strcat(t, " strcpy");
    strcat(t, " and strcat");
    printf("%s\n", t);

    return 0;
}
    strlen

    Retourne le nombre de caractères d'une chaîne.

    strcmp, strncmp

    On n'utilise pas l'opérateur == pour comparer des chaînes car ce n'est pas les adresses qu'on veut comparer mais le contenu mémoire. La fonction strcmp compare deux chaînes de caractères et retourne :

    • zéro si les chaînes sont identiques
    • un nombre négatif si la première est "inférieure" (du point de vue lexicographique) à la seconde
    • et un nombre positif si la première est "supérieure" (du même point de vue ...) à la seconde

    Ainsi, à titre d'exemple, dans l'expression

    strcmp("clandestin", "clavier")

    La fonction retourne un nombre négatif car, 'n' étant plus petit que 'v' (dans le jeu de caractères ASCII, ça n'a rien à voir avec le langage C), "clandestin" est plus petit que "clavier".

    IV-C-5. Fusion de chaînes littérales

    Le C permet de fusionner deux chaînes littérales en les plaçant simplement côte à côte. Cela se révèle particulièrement utile lorsqu'on doit composer une chaîne et que cette dernière soit trop longue a écrire sur une seule ligne. Par exemple :

    #include <stdio.h>
int main()
{
    printf( "Voici une chaine de caracteres particulierement longue, tres longue, "
            "tellement longue (oui, longue !) qu'il a fallu la decouper en deux !" );
    return 0;
}

    IV-D. Exercices

    IV-D-1. Recherche dans un tableau

    Nous nous proposons d'écrire un programme qui permet de trouver toutes les occurrences d'un nombre entré par l'utilisateur parmi un un ensemble de nombres précédemment entrés. L'utilisateur entrera dix nombres (que vous placerez dans un tableau), ensuite il entrera le nombre à rechercher et le programme devra afficher toutes les occurrences de ce nombre s'il y en a ainsi que le nombre d'occurrences trouvées. Voici un exemple d'exécution :

    Ce programme permet de trouver toutes les occurrences d'un nombre.
Entrez 10 nombres :
t[0] : 2
t[1] : -1
t[2] : 3
t[3] : 28
t[4] : 3
t[5] : -8
t[6] : 9
t[7] : 40
t[8] : -1
t[9] : 8
Entrez le nombre a rechercher : 3
t[2]
t[4]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

    IV-D-2. Calcul de la moyenne

    Nous nous proposons d'écrire un programme permettant de calculer la moyenne d'un élève. L'utilisateur entrera 5 "notes". Une note est constituée d'une note sur 20 (de type entier) et d'un coefficient compris entre 1 et 5 (entier également). La note définitive pour une matière est la note sur 20 multipliée par le coefficient. La moyenne de l'étudiant est égale au total des notes définitives divisé par la somme des coefficients.

    On créera un tableau t de 5 tableaux de 3 entiers, c'est-à-dire int t[5][3]. Les 5 éléments de t sont destinés à accueillir les 5 notes. Pour chaque note t[i], t[i][0] contiendra la note sur 20, t[i][1] le coefficient et t[i][2] la note définitive. Le programme demandera à l'utilisateur d'entrer les notes puis affichera la moyenne de l'élève avec 2 chiffres après la virgule et les calculs intermédiaires. Voici un exemple d'exécution :

    Ce programme permet de calculer votre moyenne scolaire.
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

    IV-D-3. Manipulation de chaînes

    Ecrire les fonctions de manipulation de chaînes suivantes :

    • str_cpy, de rôle et d'utilisation semblables à la fonction strcpy de la bibliothèque standard
    • str_len, de rôle et d'utilisation semblalbles à la fonction strlen de la bibliothèque standard
    • str_equals, qui reçoit en arguments de chaînes de caractères et qui retourne VRAI si elles sont égales et FAUX dans le cas contraire


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

Table des matières

  • V. Les entrées/sorties en langage C
    • V-A. Introduction
    • V-B. Les fichiers
    • V-C. Les entrée et sortie standards
    • V-D. Exemple : lire un caractère, puis l'afficher
    • V-E. Saisir une chaîne de caractères
    • V-F. Lire une ligne avec fgets
    • V-G. Mécanisme des entrées/sorties en langage C
      • V-G-1. Le tamponnage
        • V-G-1-a. Les tampons d'entrée/sortie
        • V-G-1-b. Les modes de tamponnage
      • V-G-2. Lire de manière sûre des données sur l'entrée standard

    • V. Les entrées/sorties en langage C

    V-A. Introduction

    Les entrées/sorties (E/S) ne font pas vraiment partie du langage C car ces opérations sont dépendantes du système. Cela signifie que pour réaliser des opérations d'entrée/sortie en C, il faut en principe passer par les fonctionnalités offertes par le système. Néanmoins sa bibliothèque standard est fournie avec des fonctions permettant d'effectuer de telles opérations afin de faciliter l'écriture de code portable. Les fonctions et types de données liées aux entrées/sorties sont principalement déclarés dans le fichier stdio.h (standard input/output).

    V-B. Les fichiers

    Les entrées/sorties en langage C se font par l'intermédiaire d'entités logiques, appelés flux, qui représentent des objets externes au programme, appelés fichiers. Un fichier peut être ouvert en lecture, auquel cas il est censé nous fournir des données (c'est-à-dire être lu) ou ouvert en écriture, auquel cas il est destiné à recevoir des données provenant du programme. Un fichier peut être à la fois ouvert en lecture et en écriture. Une fois qu'un fichier est ouvert, un flux lui est associé. Un flux d'entrée est un flux associé à un fichier ouvert en lecture et un flux de sortie un flux associé à un fichier ouvert un écriture. Tous les fichiers ouverts doivent être fermés avant la fin du programme.

    Lorsque les données échangées entre le programme et le fichier sont de type texte, la nécessité de définir ce qu'on appelle une ligne est primordiale. En langage C, une ligne est une suite de caractères terminée par le caractère de fin de ligne (inclus) : '\n'. Par exemple, lorsqu'on effectue des saisies au clavier, une ligne correspond à une suite de caractères terminée par ENTREE. Puisque la touche ENTREE termine une ligne, le caractère généré par l'appui de cette touche est donc, en C standard, le caractère de fin de ligne soit '\n'.

    V-C. Les entrée et sortie standards

    Lorsque le système exécute un programme, trois fichiers sont automatiquement ouverts :

    • l'entrée standard par défaut le clavier
    • la sortie standard, par défaut l'écran (ou la console)
    • et l'erreur standard, par défaut associé à l'écran (ou la console)

    Respectivement associés aux flux stdin, stdout et stderr. Ils sont automatiquement fermés avant la fin du programme.

    Chez la majorité des systèmes, dont Windows et UNIX, l'utilisateur peut rediriger les entrée et sortie standards vers un autre fichier à l'aide des symboles < et >. Par exemple, exécutez le programme qui affiche « Hello, world » à partir de l'interpréteur de commandes à l'aide de la commande suivante (nous supposons que nous sommes sous Windows et que le programme s'appelle hello.exe) :

    hello > sortie.txt

    et vous verrez que le message sera imprimé dans le fichier sortie.txt et non à l'écran. Si le fichier n'existe pas, il sera créé. S'il existe déjà, son ancien contenu sera effacé.

    Un deuxième et dernier exemple : écrire un programme qui affiche Hello, world en écrivant explicitement sur stdout, autrement dit avec un printf beaucoup plus explicite. On utilisera alors la fonction fprintf qui permet d'écrire du texte sur un flux de sortie, dans notre cas : stdout, ce qui nous donne :

    #include <stdio.h>
int main()
{
    fprintf(stdout, "Hello, world\n");
    return 0;
}

    V-D. Exemple : lire un caractère, puis l'afficher

    La macro getc permet de lire un caractère sur un flux d'entrée. La macro putc permet d'écrire un caractère sur un flux de sortie. Voici un programme simple qui montre comment utiliser les macros getc et putc :

    #include <stdio.h>
int main()
{
    int c; /* le caractere */

    printf("Veuillez taper un caractere : ");
    c = getc(stdin);
    printf("Vous avez tape : ");
    putc(c, stdout);
    return 0;
}

    Vous vous demandez certainement la raison pour laquelle on a utilisé int plutôt que char dans la déclaration de c. Et bien tout simplement parce que getc retourne un int (de même putc attend un argument de type int). Mais justement : Pourquoi ? Et bien parce que getc doit pouvoir non seulement retourner le caractère lu (un char) mais aussi une valeur qui ne doit pas être un char pour signaler qu'aucun caractère n'a pu être lu. Cette valeur est EOF. Elle est définie dans le fichier stdio.h. Dans ces conditions, il est clair qu'on peut utiliser tout sauf un char comme type de retour de getc.

    Un des cas les plus fréquents où getc retourne EOF est lorsqu'on a rencontré la fin du fichier. La fin d'un fichier est un point situé au-delà du dernier caractère de ce fichier (si le fichier est vide, le début et la fin du fichier sont donc confondus). On dit qu'on a rencontré la fin d'un fichier après avoir encore tenté de lire dans ce fichier alors qu'on se trouve déjà à la fin, pas juste après avoir lu le dernier caractère. Lorsque stdin est associé au clavier, la notion de fin de fichier perd à priori son sens car l'utilisateur peut très bien taper n'importe quoi à n'importe quel moment. Cependant l'environnement d'exécution (le système d'exploitation) offre généralement un moyen de spécifier qu'on n'a plus aucun caractère à fournir (concrètement, pour nous, cela signifie que getc va retourner EOF). Sous Windows par exemple, il suffit de taper en début de ligne la combinaison de touches Ctrl + Z (héritée du DOS) puis de valider par ENTREE. Evidemment, tout recommence à zéro à la prochaine opération de lecture.

    Les macros getchar et putchar s'utilisent comme getc et putc sauf qu'elles n'opèrent que sur stdin, respectivement stdout. Elles sont définies dans stdio.h comme suit :

    #define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)

    Et enfin fgetc est une fonction qui fait la même chose que getc (qui peut être en fait une fonction ou une macro ...). De même fputc est une fonction qui fait la même chose que putc.

    V-E. Saisir une chaîne de caractères

    Il suffit de lire les caractères présents sur le flux d'entrée (dans notre cas : stdin) jusqu'à ce que l'on ait atteint la fin du fichier ou le caractère de fin de ligne. Nous devrons fournir en arguments de la fonction l'adresse du tampon destiné à contenir la chaîne de caractère saisie et la taille de ce tampon pour supprimer le risque de débordement de tampon.

    #include <stdio.h>
char * saisir_chaine(char * lpBuffer, size_t nBufSize);
int main()
{
    char lpBuffer[20];
    printf("Entrez une chaine de caracteres : ");
    saisir_chaine(lpBuffer, sizeof(lpBuffer));

    printf("Vous avez tape : %s\n", lpBuffer);
    return 0;
}
char * saisir_chaine(char * lpBuffer, size_t nBufSize)
{
    size_t nbCar = 0;
    int c;
    c = getchar();
    while (nbCar < nBufSize - 1 && c != EOF && c != '\n')
    {
        lpBuffer[nbCar] = (char)c;
        nbCar++;
        c = getchar();
    }
    lpBuffer[nbCar] = '\0';

    return lpBuffer;
}

    La fonction scanf permet également de saisir une chaîne de caractères ne comportant aucun espace (espace, tabulation, etc.) grâce au spécificateur de format %s. Elle va donc arrêter la lecture à la rencontre d'un espace (mais avant d'effectuer la lecture, elle va d'abord avancer jusqu'au premier caractère qui n'est pas un espace). scanf ajoute enfin le caractère de fin de chaîne. Le gabarit permet d'indiquer le nombre maximum de caractères à lire (caractère de fin de chaîne non compris). Lorsqu'on utilise scanf avec le spécificateur %s (qui demande de lire une chaîne sans espace), il ne faut jamais oublier de spécifier également le nombre maximum de caractères à lire (à mettre juste devant le s) sinon le programme sera ouvert aux attaques par débordement de tampon. Voici un exemple qui montre l'utilisation de scanf avec le spécicateur de format %s :

    #include <stdio.h>
int main()
{
    char lpBuffer[20];
    printf("Entrez une chaine de caracteres : ");
    scanf("%19s", lpBuffer);
    printf("Vous avez tape : %s\n", lpBuffer);
    return 0;
}

    Et enfin, il existe également une fonction, gets, déclarée dans stdio.h, qui permet de lire une chaîne de caractères sur stdin. Cependant cette fonction est à proscrire car elle ne permet pas de spécifier la taille du tampon qui va recevoir la chaîne lue.

    V-F. Lire une ligne avec fgets

    La fonction fgets permet de lire une ligne (c'est-à-dire y compris le '\n') sur un flux d'entrée et de placer les caractères lus dans un buffer. Cette fonction ajoute ensuite le caractère '\0'. Exemple :

    #include <stdio.h>
int main()
{
    char lpBuffer[20];
    printf("Entrez une chaine de caracteres : ");
    fgets(lpBuffer, sizeof(lpBuffer), stdin);
    printf("Vous avez tape : %s", lpBuffer);
    return 0;
}

    Dans cet exemple, deux cas peuvent se présenter :

    • l'utilisateur entre une chaîne comportant 18 caractères tout au plus puis valide le tout par ENTREE, alors tous les caractères de la ligne, y compris le caractère de fin de ligne, sont copiés dans lpBuffer puis le caractère de fin de chaîne est ajouté

    • l'utilisateur entre une chaîne comportant plus de 18 caractères (c'est-à-dire >= 19) puis valide le tout par ENTREE, alors seuls les 19 premiers caractères sont copiés vers lpBuffer puis le caractère de fin de chaîne est ajouté

    V-G. Mécanisme des entrées/sorties en langage C

    V-G-1. Le tamponnage

    V-G-1-a. Les tampons d'entrée/sortie

    En langage C, les entrées/sorties sont par défaut bufférisées, c'est-à-dire que les données à lire (respectivement à écrire) ne sont pas directement lues (respectivement écrites) mais sont tout d'abord placées dans un tampon (buffer) associé au fichier. La preuve, vous avez certainement remarqué par exemple que lorsque vous entrez des données pour la première fois à l'aide du clavier, ces données ne seront lues qu'une fois que vous aurez appuyé sur la touche ENTREE. Ensuite, toutes les opérations de lecture qui suivent se feront immédiatement tant que le caractère '\n' est encore présent dans le tampon de lecture, c'est-à-dire tant qu'il n'a pas été encore lu. Lorsque le caractère '\n' n'est plus présent dans le buffer, vous devrez à nouveau appuyer sur ENTREE pour valider la saisie, et ainsi de suite.

    Les opérations d'écriture sont moins compliquées, mais il y a quand même quelque chose dont il serait totalement injuste de ne pas en parler. Comme nous l'avons déjà dit plus haut, les entrées/sorties sont par défaut bufferisées c'est-à-dire passent par un tampon. Dans le cas d'une opération d'écriture, il peut arriver que l'on souhaite à un certain moment forcer l'écriture physique des données présentes dans le tampon sans attendre que le système se décide enfin de le faire. Dans ce cas, on utilisera la fonction fflush. Nous verrons dans le paragraphe suivant un exemple d'utilisation de cette fonction.

    V-G-1-b. Les modes de tamponnage

    Le langage C permet de spécifier le mode de tamponnage à utiliser avec un flux donné à l'aide de la fonction setvbuf. Elle doit être appelée avant toute utilisation du fichier.

    Il existe 3 modes de tamponnage des entrées/sorties :

    • Pas de tamponnage (_IONBF (no buffering)), dans lequel le flux n'est associé à aucun tampon. Les données sont directement écrites sur ou lues depuis le fichier.

    • Tamponnage par lignes (_IOLBF (line buffering)), le mode par défaut (d'après la norme), dans lequel le flux est associé à un tampon vidé que lorsqu'il est plein, lorsque le caractère de fin de ligne a été envoyé, lorsqu'une opération de lecture sur un flux en mode "Pas de tampon" a été efectuée ou lorsqu'une opération de lecture sur un flux en mode "Tamponnage par lignes" nécessite son vidage. Par exemple, en pratique, lorsque stdout est dans ce mode, toute demande de lecture sur stdin provoquera l'écriture physique des caractères encore dans le tampon. Cela permet d'avoir les questions afichées à l'écran avant que l'utilisateur puisse entrer la réponse.

    • Tamponnage strict (_IOFBF (full buffering)), dans lequel le flux associé à un tampon vidé que lorsqu'il est plein.

    Dans tous les cas, la fermeture d'un fichier ouvert en écriture entraîne également l'écriture des caractères qui sont encore dans le tampon, s'il y en a (ce qui a bien évidemment lieu avant sa fermeture).

    Le prototype de la fonction setvbuf est le suivant :

int setvbuf(FILE * f, char * buf, int mode, size_t size);

    L'argument mode indique évidemment le mode à utiliser (_IONBF, _IOLBF ou _IOFBF) et l'argument size la taille du tampon (buf) à associer au flux. Si buf vaut NULL, un tampon de taille size sera alloué et associé au fichier. Les arguments buf et size sont évidemment ignorés lorsque mode vaut _IONBF.

    Voici un exemple d'utilisation de cette fonction :

    #include <stdio.h>
#define N 1
void loop(unsigned long n);
int main()
{
    setvbuf(stdout, NULL, _IOFBF, 8);
    printf("a\n"); /* Contenu du buffer : [a\n]. */
    loop(N); /* Faire une quelconque longue boucle */
    /* Le buffer n'est pas encore plein, rien ne s'affichera donc sur la sortie standard. */
    printf("b\n"); /* Contenu du buffer : [a\nb\n]. */
    fflush(stdout); /* Vider le buffer. */
    /* Le buffer n'est pas encore plein mais fflush a ete appelee. On aura donc :   */
    /* - Sur la sortie standard : + [a\nb\n].                                       */
    /* - Dans le buffer : [] (rien).                                                */
    loop(N); /* Faire une quelconque longue boucle */
    printf("azertyuiop\n");
    /* On aura :                                          */
    /* - Dans le buffer : [azertyui].                     */
    /* Le buffer est plein, le vidage s'impose. On aura : */
    /* - Sur la sortie standard : + [azertui].            */
    /* - Dans le buffer : [] (rien).                      */
    /* Il reste encore les carateres [op\n]. On aura :    */
    /* - Dans le buffer : [op\n].                         */

    loop(N); /* Faire une quelconque longue boucle */
    return 0;
    /* Au dela de l'accolade : Fin du programme.                                           */
    /* Tous les fichiers encore ouverts (dont la sortie standard) seront fermes. On aura : */
    /* - Sur la sortie standard : + [op\n].                                                */
    /* - Dans le buffer : [] (rien).                                                       */
}
void loop(unsigned long n)
{
    unsigned long i, j, end = 100000000;
    for(i = 0; i < n; i++)
        for(j = 0; j < end; j++);
}

    V-G-2. Lire de manière sûre des données sur l'entrée standard

    Tout d'abord, analysons le tout petit programme suivant :

    #include <stdio.h>
int main()
{
    char nom[12], prenom[12];
    printf("Entrez votre nom : ");
    fgets(nom, sizeof(nom), stdin);
    printf("Entrez votre prenom : ");
    fgets(prenom, sizeof(prenom), stdin);
    printf("Votre nom est : %s", nom);
    printf("Et votre prenom : %s", prenom);
    return 0;
}

    Dans ce programme, si l'utilisateur entre un nom comportant moins de 10 caractères puis valide par ENTREE, alors tous les caractères rentrent dans nom et le programme se déroule bien comme prévu. Par contre si l'utilisateur entre un nom comportant plus de 10 caractères, seuls les 11 premiers caractères seront copiés dans nom et des caractères sont donc encore présents dans le buffer du clavier. Donc, à la lecture du prénom, les caractères encore présents dans le buffer seront immédiatement lus sans que l'utilisateur n'ait pu entrer quoi que ce soit. Voici un deuxième exemple :

    #include <stdio.h>
int main()
{
    int n;
    char c;
    printf("Entrez un nombre (entier) : ");
    scanf("%d", &n);
    printf("Entrez un caractere : ");
    scanf("%c", &c);
    printf("Le nombre que vous ave entre est : %d\n", n);
    printf("Le caractere que vous ave entre est : %c\n", c);
    return 0;
}

    Lorsqu'on demande à scanf de lire un nombre, elle va déplacer le pointeur jusqu'au premier caractère non blanc, lire tant qu'elle doit lire les caractères pouvant figurer dans l'expression d'un nombre, puis s'arrêter à la rencontre d'un caractère invalide (espace ou lettre par exemple).

    Donc dans l'exemple ci-dessus, la lecture du caractère se fera sans l'intervention de l'utilisateur à cause de la présence du caractère '\n' (qui sera alors le caractère lu) due à la touche ENTREE frappée pendant la saisie du nombre.

    Ces exemples nous montrent bien que d'une manière générale, il faut toujours vider le buffer du clavier après chaque saisie, sauf si celui-ci est déjà vide bien sûr. Pour vider le buffer du clavier, il suffit de manger tous les caractères présents dans le buffer jusqu'à ce qu'on ait rencontré le caractère de fin de ligne ou atteint la fin du fichier. A titre d'exemple, voici une version améliorée (avec vidage du tampon d'entrée après lecture) de notre fonction saisir_chaine :

    char * saisir_chaine(char * lpBuffer, int nBufSize)
{
    char * ret = fgets(lpBuffer, nBufSize, stdin);

    if (ret != NULL)
    {
        char * p = lpBuffer + strlen(lpBuffer) - 1;
        if (*p == '\n')
            *p = '\0'; /* on ecrase le '\n' */
        else
        {
            /* On vide le tampon de lecture du flux stdin */
            int c;

            do
                c = getchar();
            while (c != EOF && c != '\n');
        }
    }

    return ret;
}


melem.developpez.com :: Initiation au langage C

Vu dans ... https://melem.developpez.com/tutoriels/langage-c/initiation-langage-c

Table des matières

  • VI. L'allocation dynamique de mémoire
    • VI-A. Les fonctions malloc et free
    • VI-B. La fonction realloc
    • VI-C. Exercices
      • VI-C-1. Calcul de la moyenne (version 2)
      • VI-C-2. Recherche dans un tableau (version 2)
  • VI. Solutions des exercices
    • VI-A. La lettre X (II-E-1)
    • VI-B. Périmètre d'un rectangle (II-E-2)
    • VI-C. Valeur absolue (III-G-1)
    • VI-D. Moyenne (III-G-2)
    • VI-E. L'heure dans une minute (III-G-3)
    • VI-F. Rectangle (III-G-4)
    • VI-G. Triangle isocèle (III-G-5)
    • VI-H. Somme (III-G-6)
    • VI-I. Recherche dans un tableau (IV-D-1)
    • VI-J. Calcul de la moyenne (IV-D-2)
    • VI-K. Manipulation de chaînes (IV-D-3)
    • VI-L. Calcul de la moyenne (version 2) (VI-C-1)
    • VI-M. Recherche dans un tableau (version 2) (VI-C-2)

    • VI. L'allocation dynamique de mémoire

    VI-A. Les fonctions malloc et free

    L'intérêt d'allouer dynamiquement de la mémoire se ressent lorsqu'on veut créer un tableau dont la taille dont nous avons besoin n'est connue qu'à l'exécution par exemple. On utilise généralement les fonctions malloc et free.

    int t[10];
...
/* FIN */

    Peut être remplacé par :

    int * p;
p = malloc(10 * sizeof(int));
...
free(p); /* libérer la mémoire lorsqu'on n'en a plus besoin */
/* FIN */

    Les fonctions malloc et free sont déclarées dans le fichier stdlib.h. malloc retourne NULL en cas d'échec. Voici un exemple qui illustre une bonne manière de les utiliser :

    #include <stdio.h>
#include <stdlib.h>
int main()
{
    int * p;

    /* Creation d'un tableau assez grand pour contenir 10 entiers */
    p = malloc(10 * sizeof(int));

    if (p != NULL)
    {
        printf("Succes de l'operation.\n");
        p[0] = 1;
        printf("p[0] = %d\n", p[0]);
        free(p); /* Destruction du tableau. */
    }
    else
        printf("Le tableau n'a pas pu etre cree.\n");

    return 0;
}

    VI-B. La fonction realloc

    La fonction realloc :

    void * realloc(void * memblock, size_t newsize);

    permet de « redimensionner » une mémoire allouée dynamiquement (par malloc par exemple). Si memblock vaut NULL, realloc se comporte comme malloc. En cas de réussite, cette fonction retourne alors l'adresse de la nouvelle mémoire, sinon la valeur NULL est retournée et la mémoire pointée par memblock reste inchangée.

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int * p = malloc(10 * sizeof(int));

    if (p != NULL)
    {
        /* Sauver l'ancienne valeur de p au cas ou realloc echoue. */
        int * q = p;
        /* Redimensionner le tableau. */
        p = realloc(p, 20 * sizeof(int));

        if (p != NULL)
        {
            printf("Succes de l'operation.\n");
            p[0] = 1;
            printf("p[0] = %d\n", p[0]);
            free(p);
        }
        else
        {
            printf("Le tableau n'a pas pu etre redimensionne.\n");
            free(q);
        }
    }
    else
        printf("Le tableau n'a pas pu etre cree.\n");

    return 0;
}

    VI-C. Exercices

    VI-C-1. Calcul de la moyenne (version 2)

    Reprendre l'exercice IV-D-2 en demandant cette fois-ci à l'utilisateur le nombre de notes qu'il désire entrer (limité à exactement 5 dans l'ancien sujet). Voici un exemple d'exécution :

    Ce programme permet de calculer votre moyenne scolaire.
Entrez le nombre de notes que vous voulez entrer : 5
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

    VI-C-2. Recherche dans un tableau (version 2)

    Reprenez l'exercice IV-D-1 en remplaçant les nombres par des chaînes de caractères. L'utilisateur entrera en premier lieu le nombre de chaînes qu'il va entrer.

    Aucune chaîne ne doit excéder 20 caractères mais la mémoire que vous utiliserez pour stocker chaque chaîne entrée doit être juste assez grande pour la contenir. Autrement dit, votre objectif sera de minimiser la quantité de mémoire utilisée. Voici un exemple d'exécution :

    Ce programme permet de trouver toutes les occurrences d'une chaine.
Combien de chaines voulez-vous entrer ? 10
Entrez 10 chaines de caracteres (20 caracteres par chaine au plus) :
t[0] : programmation
t[1] : langage
t[2] : C
t[3] : ANSI
t[4] : ISO
t[5] : IEC
t[6] : programmation
t[7] : programme
t[8] : code
t[9] : programme
Entrez la chaine a rechercher : programmation
t[0]
t[6]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

    VI. Solutions des exercices

    VI-A. La lettre X (II-E-1)

    Voici la solution la plus évidente et la plus lisible :

    #include <stdio.h>
int main()
{
    printf("*   *\n");
    printf(" * * \n");
    printf("  *  \n");
    printf(" * * \n");
    printf("*   *\n");
    return 0;
}

    On peut cependant remarquer que les espaces après le dernier * de chaque ligne sont inutiles. De plus, rien ne nous oblige à faire un appel à printf pour chaque ligne. On peut faire tout le dessin avec un seul appel à cette fonction. Voici donc une autre solution, un peu moins évidente que la première :

    #include <stdio.h>
int main()
{
    printf("*   *\n * *\n  *\n * *\n*   *\n");
    return 0;
}

    VI-B. Périmètre d'un rectangle (II-E-2)

    #include <stdio.h>
#define LONGUEUR 100
#define HAUTEUR 60
int perimetre(int L, int h);
int main()
{
    printf(
        "Le perimetre d'un rectangle de longueur %d et de hauteur %d est %d.\n",
        LONGUEUR, HAUTEUR, perimetre(LONGUEUR, HAUTEUR)
    );
    return 0;
}
int perimetre(int L, int h)
{
    return 2 * (L + h);
}

    Ayez l'habitude de représenter les valeurs figées par des macros. Cela permet d'avoir un code à la fois lisible et facilement maintenable : si un jour la spécification change, il suffit de changer les définitions des macros et non plusieurs parties du programme.

    VI-C. Valeur absolue (III-G-1)

    Il y a plusieurs façons d'y arriver. Voici deux exemples :

    #include <stdio.h>
int main()
{
    double x, y;
    printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");
    printf("Entrez un nombre : ");
    scanf("%lf", &x);
    if (x >= 0)
        y = x;
    else
        y = -x;
    printf("La valeur absolue de %f est : %f\n", x, y);
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    #include <stdio.h>
int main()
{
    double x;
    printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");
    printf("Entrez un nombre : ");
    scanf("%lf", &x);
    printf("La valeur absolue de %f est : %f\n", x, (x >= 0 ? x : -x));
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-D. Moyenne (III-G-2)

    #include <stdio.h>
int main()
{
    int note;
    printf("Ce programme permet de determiner si vous avez eu la moyenne ou non.\n");
    printf("Entrez votre note (0 a 20) : ");
    scanf("%d", &note);
    if (0 <= note && note <= 20)
    {
        if (note >= 10)
            printf("Vous avez eu la moyenne.\n");
        else
            printf("Vous n'avez pas eu la moyenne.\n");
    }
    else
        printf("Cette note n'est pas valide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-E. L'heure dans une minute (III-G-3)

    #include <stdio.h>
int main()
{
    int h, m;
    printf("Ce programme permet de determiner l'heure qu'il sera une minute plus tard.\n");
    printf("Entrez l'heure actuelle : ");
    scanf("%d", &h);
    printf("Entrez la minute actuelle : ");
    scanf("%d", &m);
    m++;
    if (m == 60)
    {
        m = 0;
        h++;
        if (h == 24)
            h = 0;
    }
    printf("Dans une minute il sera : %02d:%02d\n", h, m);
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-F. Rectangle (III-G-4)

    Le programme a est complet. Pour les programmes b et c, seule la fonction main() est donnée (les autres parties du programme restant inchangées par rapport à a).

    a.

    #include <stdio.h>
void affiche_car(char c, int n);
int main()
{
    int L;
    printf("Ce programme dessine une ligne.\n");
    printf("Entrez la longueur de la ligne (0 a 20) : ");
    scanf("%d", &L);
    if (0 <= L && L <= 20)
    {
        affiche_car('*', L);
        putchar('\n');
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}
void affiche_car(char c, int n)
{
    if (c > 0 && n > 0)
    {
        int i; /* i : nombres de caracteres deja affiches */
        for(i = 0; i < n; i++) /* on arrete des que i a atteint n */
            putchar(c);
    }
}

    Vous ne vous attendiez certainement pas à ce que la valeur de c soit testée avant de le passer à putchar. La raison est tout simplement que l'argument attendu par putchar est un int (qui doit avoir une valeur positive) et non un char. Comme char peut faire référence à signed char, il vaut mieux tester la valeur du caractère reçu avant de faire quoi que ce soit avec. putchar sera étudiée dans plus de détails dans le chapitre V.

    b.

    int main()
{
    int L;
    printf("Ce programme dessine un rectangle plein.\n");
    printf("Entrez la longueur du rectangle (0 a 20) : ");
    scanf("%d", &L);
    if (0 <= L && L <= 20)
    {
        int h;
        printf("Entrez la hauteur du rectangle (0 a 20) : ");
        scanf("%d", &h);
        if (0 <= h && h <= 20)
        {
            /* On dessine h lignes (lignes 1 a h) */

            int i; /* i : numero de la ligne en cours */
            for(i = 1; i <= h; i++)
            {
                affiche_car('*', L);
                putchar('\n');
            }
        }
        else
            printf("Cette valeur est invalide.\n");
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    c.

    int main()
{
    int L;
    printf("Ce programme dessine un rectangle creux.\n");
    printf("Entrez la longueur du rectangle (0 a 20) : ");
    scanf("%d", &L);
    if (0 <= L && L <= 20)
    {
        int h;
        printf("Entrez la hauteur du rectangle (0 a 20) : ");
        scanf("%d", &h);
        if (0 <= h && h <= 20)
        {
            int i;
            for(i = 1; i <= h; i++)
            {
                /* Pour les lignes 1 et h, dessiner une ligne continue */
                if (i == 1 || i == h)
                    affiche_car('*', L);
                else
                {
                    /* Pour le reste, seules les extreimites sont visibles */
                    putchar('*');
                    affiche_car(' ', L - 2);
                    putchar('*');
                }
                putchar('\n');
            }
        }
        else
            printf("Cette valeur est invalide.\n");
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-G. Triangle isocèle (III-G-5)

    Ici aussi, seule la fonction main() est donnée pour les mêmes raisons que dans VII-F.

    a. Il faut juste remarquer qu'à la ligne i (i allant de 1 à h), il y a h - i espaces suivi de 1 + 2 * (i - 1) étoiles.

    int main()
{
    int h;
    printf("Ce programme dessine un triangle.\n");
    printf("Entrez la hauteur du triangle (0 a 20) : ");
    scanf("%d", &h);
    if (0 <= h && h <= 20)
    {
        int i;
        for(i = 1; i <= h; i++)
        {
            affiche_car(' ', h - i);
            affiche_car('*', 2 * i - 1);
            putchar('\n');
        }
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    b.

    int main()
{
    int h;
    printf("Ce programme dessine un triangle.\n");
    printf("Entrez la hauteur du triangle (0 a 20) : ");
    scanf("%d", &h);
    if (0 <= h && h <= 20)
    {
        int i, n, nb_etoiles; /* nb_etoiles : nombre d'etoiles deja affichees */
        for(i = 1, nb_etoiles = 0; i <= h; i++)
        {
            n = h - i;
            affiche_car(' ', n);
            n = 2 * i - 1;
            affiche_car('*', n);
            nb_etoiles += n;
            putchar('\n');
        }
        printf("Il y a %d etoile(s) affichee(s).\n", nb_etoiles);
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-H. Somme (III-G-6)

    #include <stdio.h>
int main()
{
    int n, saisis = 0, fini = 0, total = 0;
    printf("Ce programme permet de calculer une somme.\n");
    printf("Entrez la liste des nombres a additionner (terminez en entrant 0).\n");
    while (!fini)
    {
        scanf("%d", &n);
        saisis++;
        total += n;
        fini = (saisis == 10 || n == 0);
    }
    printf("Le total est : %d\n", total);
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-I. Recherche dans un tableau (IV-D-1)

    #include <stdio.h>
int main()
{
    int t[10], i, n, nb_occurrences = 0;
    printf("Ce programme permet de trouver toutes les occurrences d'un nombre.\n");
    printf("Entrez 10 nombres :\n");
    for(i = 0; i < 10; i++)
    {
        printf("t[%d] : ", i);
        scanf("%d", t + i);
    }
    printf("Entrez le nombre a rechercher : ");
    scanf("%d", &n);
    for(i = 0; i < 10; i++)
    {
        if (t[i] == n)
        {
            printf("t[%d]\n", i);
            nb_occurrences++;
        }
    }
    printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-J. Calcul de la moyenne (IV-D-2)

    #include <stdio.h>
int main()
{
    int t[5][3], i, total_coefs = 0, total_notes = 0;
    double moyenne;
    printf("Ce programme permet de calculer votre moyenne scolaire.\n");
    for(i = 0; i < 5; i++)
    {
        /* Saisie des notes */
        printf("Note %d : ", i + 1);
        scanf("%d", &(t[i][0]));
        printf("Coef %d : ", i + 1);
        scanf("%d", &(t[i][1]));
        t[i][2] = t[i][0] * t[i][1];
        /* Calcul des totaux */
        total_notes += t[i][2];
        total_coefs += t[i][1];
    }
    moyenne = ((double)total_notes) / total_coefs;
    /* Affichage du resultat */
    printf("+------+------+----------+\n");
    printf("| Note | Coef | Note Def |\n");
    printf("+------+------+----------+\n");
    for(i = 0; i < 5; i++)
    {
        printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);
        printf("+------+------+----------+\n");
    }
    printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);
    printf("+------+-----------------+\n");
    printf("| Moy. | %15.2f |\n", moyenne);
    printf("+------+-----------------+\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-K. Manipulation de chaînes (IV-D-3)

    char * str_cpy(char * dest, const char * source)
{
    int i;

    for(i = 0; source[i] != '\0'; i++)
        dest[i] = source[i];

    dest[i] = '\0';

    return dest;
}
size_t str_len(const char * t)
{
    size_t len;

    for(len = 0; t[len] != '\0'; len++)
        /* On ne fait rien, on laisse seulement boucler */ ;

    return len;
}
int str_equals(const char * s1, const char * s2)
{
    int i = 0, equals = 1;
    while (equals && s1[i] != '\0' && s2[i] != '\0')
    {
        if (s1[i] != s2[i])
            equals = 0;
        i++;
    }
    if (equals && (s1[i] != '\0' || s2[i] != '\0'))
        equals = 0;
    return equals;
}

    Remarquez bien que nous avons choisi char * comme type de retour de str_cpy et non void. La valeur retournée n'est autre que la valeur du pointeur dest. Le prototype de str_cpy est strictement le même que celui de la fonction standard strcpy. Cela permet d'écrire du code compact lorsqu'on a envie, par exemple :

    char s[50];
strcat(strcpy(s, "Bonjour"), " tout le monde !");

    Le deuxième argument de la fonction (s2) est déclaré const char * et non simplement char *. En effet, les données pointées par s2 doivent être uniquement lues par la fonction, cette dernière ne doit pas avoir le droit de les modifier. L'ajout du qualificateur const ici est donc plus que recommandé. De plus, cela a un aspect documentaire : la présence ou l'abscence de const permet aux utilisateurs d'une fonction de savoir quelles données pourraient être modifiées par la fonction et quelles données seront justes lues.

    VI-L. Calcul de la moyenne (version 2) (VI-C-1)

    #include <stdio.h>
#include <stdlib.h>
typedef int NOTE[3];
int main()
{
    int n;
    printf("Ce programme permet de calculer votre moyenne scolaire.\n");
    printf("Entrez le nombre de notes que vous voulez entrer : ");
    scanf("%d", &n);
    if (n > 0)
    {
        NOTE * t = malloc(n * sizeof(NOTE));
        if (t != NULL)
        {
            int i, total_coefs = 0, total_notes = 0;
            double moyenne;
            for(i = 0; i < n; i++)
            {
                printf("Note %d : ", i + 1);
                scanf("%d", &(t[i][0]));
                printf("Coef %d : ", i + 1);
                scanf("%d", &(t[i][1]));
                t[i][2] = t[i][0] * t[i][1];
                total_notes += t[i][2];
                total_coefs += t[i][1];
            }
            moyenne = ((double)total_notes) / total_coefs;
            printf("+------+------+----------+\n");
            printf("| Note | Coef | Note Def |\n");
            printf("+------+------+----------+\n");
            for(i = 0; i < n; i++)
            {
                printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);
                printf("+------+------+----------+\n");
            }
            free(t);
            printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);
            printf("+------+-----------------+\n");
            printf("| Moy. | %15.2f |\n", moyenne);
            printf("+------+-----------------+\n");
        }
        else
            printf("Le tableau n'a pas pu etre cree.\n");
    }
    else
        printf("Cette valeur est invalide.\n");
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}

    VI-M. Recherche dans un tableau (version 2) (VI-C-2)

    #include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef char * PCHAR;
size_t get_string(char * lpBuffer, int nBufSize);
int main()
{
    int n;
    char s[21];
    printf("Ce programme permet de trouver toutes les occurrences d'une chaine.\n");
    printf("Combien de chaines voulez-vous entrer ? ");
    scanf("%d", &n);
    get_string(s, sizeof(s)); /* Pour purger stdin */
    if (n > 0)
    {
         PCHAR * t = malloc(n * sizeof(PCHAR));
         if (t != NULL)
         {
            int i, nb_occurrences = 0, succes = 1;
            printf("Entrez %d chaines de caracteres (20 caracteres par chaine au plus) :\n", n);
            for(i = 0; succes && i < n; i++)
            {
                size_t len;

                printf("t[%d] : ", i);
                len = get_string(s, sizeof(s));
                t[i] = malloc(len + 1);
                if (t[i] != NULL)
                    strcpy(t[i], s);
                else
                {
                    int j;
                    for(j = 0; j < i; j++)
                        free(t[j]);
                    succes = 0;
                    printf("Memoire insuffisante pour continuer.\n");
                }
            }
            if (succes)
            {
                printf("Entrez la chaine a rechercher : ");
                get_string(s, sizeof(s));
                for(i = 0; i < n; i++)
                {
                    if (strcmp(t[i], s) == 0)
                    {
                        printf("t[%d]\n", i);
                        nb_occurrences++;
                    }
                    free(t[i]);
                }
                printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);
            }
            free(t);
         }
         else
            printf("Le tableau n'a pas pu etre cree.\n");
    }
    printf("Merci d'avoir utilise ce programme. A bientot !\n");
    return 0;
}
size_t get_string(char * lpBuffer, int nBufSize)
{
    size_t len = 0;
    if (fgets(lpBuffer, nBufSize, stdin) != NULL)
    {
        char * p;
        len = strlen(lpBuffer);
        p = lpBuffer + len - 1;
        if (*p == '\n')
        {
            *p = '\0'; /* on ecrase le '\n' */
            len--;
        }
        else
        {
            /* On vide le tampon de lecture du flux stdin */
            int c;
            do
                c = getchar();
            while (c != EOF && c != '\n');
        }
    }

    return len;
}


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • Page 1
    • I. Eléments de base
      • I-A. Structure générale d'un programme
      • I-B. Considérations lexicales
        • I-B-1. Présentation du texte du programme
        • I-B-2. Mots-clés
        • I-B-3. Identificateurs
        • I-B-4. Opérateurs
      • I-C. Constantes littérales
        • I-C-1. Nombres entiers
        • I-C-2. Nombres flottants
        • I-C-3. Caractères et chaines de caractères
        • I-C-4. Expressions constantes
      • I-D. Types fondamentaux
        • I-D-1. Nombres entiers et caractères
        • I-D-2. Types énumérés
        • I-D-3. Nombres flottants
      • I-E. Variables
        • I-E-1. Syntaxe des déclarations
        • I-E-2. Visibilité des variables
        • I-E-3. Allocation et durée de vie des variables
        • I-E-4. Initialisation des variables
        • I-E-5. Variables locales statiques
        • I-E-6. Variables critiques
        • I-E-7. Variables constantes et volatiles
      • I-F. Variables, fonctions et compilation séparée
        • I-F-1. Identificateurs publics et privés
        • I-F-2. Déclaration d'objets externes
  • Page 2
    • II. Opérateurs et expressions
      • II-A. Généralités
        • II-A-1. Lvalue et rvalue
        • II-A-2. Priorité des opérateurs
      • II-B. Présentation détaillée des opérateurs
        • II-B-1. Appel de fonction ()
        • II-B-2. Indexation []
        • II-B-3. Sélection .
        • II-B-4. Sélection dans un objet pointé ->
        • II-B-5. Négation !
        • II-B-6. Complément à 1 ~
        • II-B-7. Les célèbres ++ et --
        • II-B-8. Moins unaire -
        • II-B-9. Indirection *
        • II-B-10. Obtention de l'adresse &
        • II-B-11. Opérateur sizeof
        • II-B-12. Conversion de type (\cast" operator)
        • II-B-13. Opérateurs arithmétiques
        • II-B-14. Décalages << >>
        • II-B-15. Comparaisons == != < <= > >=
        • II-B-16. Opérateurs de bits & | ^
        • II-B-17. Connecteurs logiques && et ||
        • II-B-18. Expression conditionnelle ? :
        • II-B-19. Affectation =
        • II-B-20. Autres opérateurs d'affectation += *= etc.
        • II-B-21. L'opérateur virgule ,
      • II-C. Autres remarques
        • II-C-1. Les conversions usuelles
        • II-C-2. L'ordre d'évaluation des expressions
        • II-C-3. Les opérations non abstraites
  • Page 3
    • III. Instructions
      • III-A. Syntaxe
      • III-B. Présentation détaillée des instructions
        • III-B-1. Blocs
        • III-B-2. Instruction-expression
        • III-B-3. Etiquettes et instruction goto
        • III-B-4. Instruction if...else...
        • III-B-5. Instructions while et do...while
        • III-B-6. Instruction for
        • III-B-7. Instruction switch
        • III-B-8. Instructions break et continue
        • III-B-9. Instruction return
  • Page 4
    • IV. Fonctions
      • IV-A. Syntaxe ANSI ou "avec prototype"
        • IV-A-1. Définition
        • IV-A-2. Type de la fonction et des arguments
        • IV-A-3. Appel des fonctions
        • IV-A-4. Déclaration "externe" d'une fonction
      • IV-B. Syntaxe originale ou "sans prototype"
        • IV-B-1. Déclaration et définition
        • IV-B-2. Appel
        • IV-B-3. Coexistence des deux syntaxes
      • IV-C. Arguments des fonctions
        • IV-C-1. Passage des arguments
        • IV-C-2. Arguments de type tableau
        • IV-C-3. Arguments par adresse
        • IV-C-4. Arguments en nombre variable
  • Page 5
    • V. Objets structurés
      • V-A. Tableaux
        • V-A-1. Cas général
        • V-A-2. Initialisation des tableaux
        • V-A-3. Chaines de caractères
      • V-B. Structures et unions
        • V-B-1. Structures
        • V-B-2. Unions
        • V-B-3. Champs de bits
      • V-C. Enumérations
      • V-D. Déclarateurs complexes
        • V-D-1. Cas des déclarations
        • V-D-2. Pointeurs et tableaux constants et volatils
        • V-D-3. La déclaration typedef
        • V-D-4. Cas des types désincarnés
  • Page 6
    • VI. Pointeurs
      • VI-A. Généralités
        • VI-A-1. Déclaration et initialisation des pointeurs
        • VI-A-2. Les pointeurs génériques et le pointeur NULL
      • VI-B. Les pointeurs et les tableaux
        • VI-B-1. Arithmétique des adresses, indirection et indexation
        • VI-B-2. Tableaux dynamiques
        • VI-B-3. Tableaux multidimensionnels
        • VI-B-4. Tableaux multidimensionnels dynamiques
        • VI-B-5. Tableaux de chaines de caractères
        • VI-B-6. Tableaux multidimensionnels formels
        • VI-B-7. Tableaux non nécessairement indexés à partir de zéro
        • VI-B-8. Matrices non dynamiques de taille inconnue
      • VI-C. Les adresses des fonctions
        • VI-C-1. Les fonctions et leurs adresses
        • VI-C-2. Fonctions formelles
        • VI-C-3. Tableaux de fonctions
        • VI-C-4. Flou artistique
      • VI-D. Structures récursives
        • VI-D-1. Déclaration
        • VI-D-2. Exemple
        • VI-D-3. Structures mutuellement récursives
  • Page 7
    • VII. Entrées-sorties
      • VII-A. Flots
        • VII-A-1. Fonctions générales sur les flots
        • VII-A-2. Les unités standard d'entrée-sortie
      • VII-B. Lecture et écriture textuelles
        • VII-B-1. Lecture et écriture de caractères et de chaines
        • VII-B-2. Ecriture avec format printf
        • VII-B-3. Lecture avec format scanf
        • VII-B-4. A propos de la fonction scanf et des lectures interactives
        • VII-B-5. Les variantes de printf et scanf
      • VII-C. Opérations en mode binaire
        • VII-C-1. Lecture-écriture
        • VII-C-2. Positionnement dans les fichiers
      • VII-D. Exemples
        • VII-D-1. Fichiers "en vrac"
        • VII-D-2. Fichiers binaires et fichiers de texte
        • VII-D-3. Fichiers en accès relatif
      • VII-E. Les fichiers de bas niveau d'UNIX
  • Page 8
    • VIII. Autres éléments du langage C
      • VIII-A. Le préprocesseur
        • VIII-A-1. Inclusion de fichiers
        • VIII-A-2. Définition et appel des "macros"
        • VIII-A-3. Compilation conditionnelle
      • VIII-B. La modularité de C
        • VIII-B-1. Fichiers en-tête
        • VIII-B-2. Exemple : stdio.h
      • VIII-C. Deux ou trois choses bien pratiques...
        • VIII-C-1. Les arguments du programme principal
        • VIII-C-2. Branchements hors fonction : setter.h
        • VIII-C-3. Interruptions : signal.h
      • VIII-D. La bibliothèque standard
        • VIII-D-1. Aide à la mise au point : assert.h
        • VIII-D-2. Fonctions utilitaires : stdlib.h
        • VIII-D-3. Traitement de chaines : string.h
        • VIII-D-4. Classification des caractères : ctype.h
        • VIII-D-5. Fonctions mathématiques : math.h
        • VIII-D-6. Limites propres à l'implémentation : limits.h, float.h

Cours complet sur le langage C écrit par Henri Garreta, du Département d'Informatique de la Faculté des Sciences de Luminy - Université de la Méditerranée (Aix-Marseille).

Il est composé des 8 chapitres suivants :

  1. Eléments de base
  2. Opérateurs et expressions
  3. Instructions
  4. Fonctions
  5. Objets structurés
  6. Pointeurs
  7. Entrées-sorties
  8. Autres éléments du langage C


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • I. Eléments de base
    • I-A. Structure générale d'un programme
    • I-B. Considérations lexicales
      • I-B-1. Présentation du texte du programme
      • I-B-2. Mots-clés
      • I-B-3. Identificateurs
      • I-B-4. Opérateurs
    • I-C. Constantes littérales
      • I-C-1. Nombres entiers
      • I-C-2. Nombres flottants
      • I-C-3. Caractères et chaines de caractères
      • I-C-4. Expressions constantes
    • I-D. Types fondamentaux
      • I-D-1. Nombres entiers et caractères
      • I-D-2. Types énumérés
      • I-D-3. Nombres flottants
    • I-E. Variables
      • I-E-1. Syntaxe des déclarations
      • I-E-2. Visibilité des variables
      • I-E-3. Allocation et durée de vie des variables
      • I-E-4. Initialisation des variables
      • I-E-5. Variables locales statiques
      • I-E-6. Variables critiques
      • I-E-7. Variables constantes et volatiles
    • I-F. Variables, fonctions et compilation séparée
      • I-F-1. Identificateurs publics et privés
      • I-F-2. Déclaration d'objets externes

I. Eléments de base

I-A. Structure générale d'un programme

La transformation d'un texte écrit en langage C en un programme exécutable par l'ordinateur se fait en deux étapes : la compilation et l'édition de liens. La compilation est la traduction des fonctions écrites en C en des procédures équivalentes écrites dans un langage dont la machine peut exécuter les instructions. Le compilateur lit toujours un fichier, appelé fichier source, et produit un fichier, dit fichier objet.

Chaque fichier objet est incomplet, insuffisant pour être exécuté, car il contient des appels de fonctions ou des références à des variables qui ne sont pas définies dans le même fichier. Par exemple, le premier programme que vous écrirez contiendra déjà la fonction printf que vous n'aurez certainement pas écrite vous-même. L'édition de liens est l'opération par laquelle plusieurs fichiers objets sont mis ensemble pour se compléter mutuellement : un fichier apporte des définitions de fonctions et de variables auxquelles un autre fichier fait référence et réciproquement. L'éditeur de liens (ou linker ) prend en entrée plusieurs fichiers objets et bibliothèques (une variété particulière de fichiers objets) et produit un unique fichier exécutable. L'éditeur de liens est largement indépendant du langage de programmation utilisé pour écrire les fichiers sources, qui peuvent même avoir été écrits dans des langages différents.

Chaque fichier source entrant dans la composition d'un programme exécutable est fait d'une succession d'un nombre quelconque d'éléments indépendants, qui sont :

  • des directives pour le préprocesseur (lignes commençant par #),
  • des constructions de types (struct, union, enum, typedef),
  • des déclarations de variables et de fonctions externes,
  • des définitions de variables et
  • des définitions de fonctions.

Seules les expressions des deux dernières catégories font grossir le fichier objet : les définitions de fonctions laissent leur traduction en langage machine, tandis que les définitions de variables se traduisent par des réservations d'espace, éventuellement garni de valeurs initiales. Les autres directives et déclarations s'adressent au compilateur et il n'en reste pas de trace lorsque la compilation est finie.

En C on n'a donc pas une structure syntaxique englobant tout, comme la construction « Program ... end. » du langage Pascal ; un programme n'est qu'une collection de fonctions assortie d'un ensemble de variables globales. D'où la question : par ou l'exécution doit-elle commencer ? La règle généralement suivie par l'éditeur de liens est la suivante : parmi les fonctions données il doit en exister une dont le nom est main. C'est par elle que l'exécution commencera ; le lancement du programme équivaut à l'appel de cette fonction par le système d'exploitation. Notez bien que, à part cela, main est une fonction comme les autres, sans aucune autre propriété spécifique ; en particulier, les variables internes à main sont locales, tout comme celles des autres fonctions. Pour finir cette entrée en matière, voici la version C du célèbre programme-qui-dit-bonjour, sans lequel on ne saurait commencer un cours de programmation1 :

Sélectionnez

#include <stdio.h>

    int main() {
    printf("Bonjour\n");
    return 0;
}

1Le programme montré ici est écrit selon des règles strictes. En fait, la plupart des compilateurs acceptent que main soit déclarée void au lieu de int, ou que ce type ne figure pas, et que l'instruction « return 0 ; » n'apparaisse pas explicitement.

I-B. Considérations lexicales

I-B-1. Présentation du texte du programme

Le programmeur est maitre de la disposition du texte du programme. Des blancs, des tabulations et des sauts à la ligne peuvent être placés à tout endroit ou cela ne coupe pas un identificateur, un nombre ou un symbole composé2.

Les commentaires commencent par /* et se terminent par */ :

Sélectionnez

/* Ce texte est un commentaire et sera donc
ignoré par le compilateur */

Les commentaires ne peuvent pas être imbriqués : écrit dans un programme, le texte /* voici un grand /* et un petit */ commentaire */ est erroné, car seul /* voici un grand /* et un petit */sera vu comme un commentaire par le compilateur.

Les langages C et C++ cohabitant dans la plupart des compilateurs actuels, ces derniers acceptent également comme commentaire tout texte compris entre le signe // et la fin de la ligne ou ce signe apparait :

Sélectionnez

// Ceci est un commentaire à la mode C++.

Le caractère anti-slash \ précédant immédiatement un saut à la ligne masque ce dernier : la ligne suivante est considérée comme devant être concaténée à la ligne courante. Cela est vrai en toute circonstance, y compris à l'intérieur d'une chaine de caractères. Par exemple, le texte

Sélectionnez

message = "anti\
constitutionnellement";

est compris comme ceci : « message = "anti constitutionnellement" ; »

2 Néanmoins, les directives pour le préprocesseur (cf. section VIII.A) doivent comporter un # dans la première position de la ligne. Cela ne constitue pas une exception à la règle donnée ici, car le préprocesseur n'est pas le compilateur C et ne travaille pas sur la syntaxe du langage.

I-B-2. Mots-clés

Les mots suivants sont réservés. Leur fonction est prévue par la syntaxe de C et ils ne peuvent pas être utilisés dans un autre but :

auto | break | case | char | const | continue | default | do
double | else | enum | extern | float | for | goto | if
int | long | register | return | short | signed | sizeof | static
struct | switch | typedef | union | unsigned | void | volatile | while

I-B-3. Identificateurs

Un identificateur est une suite de lettres et chiffres contigus, dont le premier est une lettre. Lorsque seul le compilateur est concerné, c'est-à-dire lorsqu'il s'agit d'identificateurs dont la portée est incluse dans un seul fichier (nous dirons de tels identificateurs qu'ils sont privés) :

  • en toute circonstance une lettre majuscule est tenue pour différente de la lettre minuscule correspondante ;
  • dans les identificateurs, le nombre de caractères discriminants est au moins de 31.

Attention, lorsqu'il s'agit d'identificateurs externes, c'est-à-dire partagés par plusieurs fichiers sources, il est possible que sur un système particulier l'éditeur de liens sous-jacent soit trop rustique pour permettre le respect de ces deux prescriptions.

Le caractère _ (appelé « blanc souligné ») est considéré comme une lettre ; il peut donc figurer à n'importe quelle place dans un identificateur. Cependant, par convention un programmeur ne doit pas utiliser des identificateurs qui commencent par ce caractère. Cela assure qu'il n'y aura jamais de conflit avec les noms introduits (à travers les fichiers « .h ») pour les besoins des bibliothèques, car ces noms commencent par un tel blanc souligné. ~

I-B-4. Opérateurs

Symboles simples :

Sélectionnez

( 	) 	[ 	] 	. 	! 	~ 	< 	> 	? 	:
= 	, 	+ 	- 	* 	/ 	% 	| 	& 	^

Symboles composés :

Sélectionnez

-> 	++ 	-- 	<= 	>= 	== 	!= 	&& 	|| 	<< 	>>
+= 	-= 	*= 	/= 	%= 	<<= >>= |= 	&= 	^=

Tous ces symboles sont reconnus par le compilateur comme des opérateurs. Il est interdit d'insérer des caractères blancs à l'intérieur d'un symbole composé. En outre, il est conseillé d'encadrer par des blancs toute utilisation d'un opérateur. Dans certaines circonstances cette règle est plus qu'un conseil, car sa non-observance crée une expression ambigüe.

I-C. Constantes littérales

I-C-1. Nombres entiers

Les constantes littérales numériques entières ou réelles suivent les conventions habituelles, avec quelques particularités.

Les constantes littérales sont sans signe : l'expression -123 est comprise comme l'application de l'opérateur unaire - à la constante 123 ; mais puisque le calcul est fait pendant la compilation, cette subtilité n'a aucune conséquence pour le programmeur. Notez aussi qu'en C original, comme il n'existe pas d'opérateur + unaire, la notation +123 est interdite.

Les constantes littérales entières peuvent aussi s'écrire en octal et en hexadécimal :

  • une constante écrite en octal (base 8) commence par 0 (zéro) ;
  • une constante écrite en hexadécimal (base 16) commence par 0x ou 0X.

Voici par exemple trois manières d'écrire le même nombre :

Sélectionnez

    27 	033 	0x1B

Détail à retenir : on ne doit pas écrire de zéro non significatif à gauche d'un nombre : 0123 ne représente pas la même valeur que 123.

Le type d'une constante entière est le plus petit type dans lequel sa valeur peut être représentée. Ou, plus exactement :

  • si elle est décimale : si possible int, sinon long, sinon unsigned long ;
  • si elle est octale ou hexadécimale : si possible int, sinon unsigned int, sinon unsigned long.

Certains suffixes permettent de changer cette classification :

  • U, u : indique que la constante est d'un type unsigned ;
  • L, l : indique que la constante est d'un type long.

Exemples : 1L, 0x7FFFU. On peut combiner ces deux suffixes : 16UL.

I-C-2. Nombres flottants

Une constante littérale est l'expression d'un nombre flottant si elle présente, dans l'ordre :

  • une suite de chiffres décimaux (la partie entière),
  • un point, qui joue le rôle de virgule décimale,
  • une suite de chiffres décimaux (la partie fractionnaire),
  • une des deux lettres E ou e,
  • éventuellement un signe + ou -,
  • une suite de chiffres décimaux.

Les trois derniers éléments forment l'exposant. Exemple : 123.456E-78.

On peut omettre :

  • la partie entière ou la partie fractionnaire, mais pas les deux,
  • le point ou l'exposant, mais pas les deux.

Exemples : .5e7, 5.e6, 5000000., 5e6

Une constante flottante est supposée de type double, à moins de comporter un suffixe explicite :

  • les suffixes F ou f indiquent qu'elle est du type float ;
  • les suffixes L ou l indiquent qu'elle est du type long double.

Exemples : 1.0L, 5.0e4f

I-C-3. Caractères et chaines de caractères

Une constante de type caractère se note en écrivant le caractère entre apostrophes. Une constante de type chaine de caractères se note en écrivant ses caractères entre guillemets. Exemples, trois caractères :

Sélectionnez

'A' 	'2' 	'"'

Quatre chaines de caractères :

Sélectionnez

"A" 	"Bonjour à tous !" 	"" 	"'"

On peut faire figurer n'importe quel caractère, même non imprimable, dans une constante caractère ou chaine de caractères en utilisant les combinaisons suivantes, appelées séquences d'échappement :

  • \n : nouvelle ligne (LF)
  • \t : tabulation (HT)
  • \b : espace-arrière (BS)
  • \r : retour-chariot (CR)
  • \f : saut de page (FF)
  • \a : signal sonore (BELL)
  • \\ : \
  • ' : '
  • " : "
  • \d3d2d1 : le caractère qui a pour code le nombre octal d3d2d1. S'il commence par un ou deux zéros et si cela ne crée pas une ambiguïté, on peut aussi le noter \d2d1 ou \d1

Par exemple, la chaine suivante définit la suite des 9 caractères 3 A, escape (de code ASCII 27), B, ", C, saut de page, D, \ et E :

Sélectionnez

"A\033B\"C\fD\\E"

Une constante de type caractère appartient au type char, c'est-à-dire entier représenté sur un octet. La valeur d'une constante caractère est le nombre qui représente le caractère de manière interne ; de nos jours il s'agit presque toujours du code ASCII 4.

Une constante de type chaine de caractères représente une suite finie de caractères, de longueur quelconque. Le codage interne d'une chaine de caractères est le suivant (voyez la figure 1) :

  • les caractères constituant la chaine sont rangés en mémoire, de manière contigüe, dans l'ordre ou ils figurent dans la chaine ;
  • un caractère nul est ajouté immédiatement après le dernier caractère de la chaine, pour en indiquer la fin ;
  • la constante chaine représente alors, à l'endroit ou elle est écrite, l'adresse de la cellule ou a été rangé le premier caractère de la chaine
Fig. 1 - Représentation de la chaine 'Bonjour'
Fig. 1 - Représentation de la chaine 'Bonjour'

Par conséquent, une constante chaine de caractères a pour type celui d'un tableau de caractères (c'est-à-dire « char[] ») et pour valeur l'adresse d'une cellule de la mémoire. Par caractère nul on entend le caractère dont le code interne est 0 ; on peut le noter indifféremment 0, '\000' ou '\0' (mais certainement pas '0') ; il est utilisé très fréquemment en C. Notez que, dans une expression, '\0' est toujours interchangeable avec 0.

3Nous verrons qu'en fait cette chaine comporte un caractère de plus qui en marque la fin.

4En standard le langage C ne prévoit pas le codage Unicode des caractères.

I-C-4. Expressions constantes

Une expression constante est une expression de l'un des types suivants :

  • toute constante littérale ; exemples : 1, 'A', "HELLO", 1.5e-2 ;
  • une expression correcte formée par l'application d'un opérateur courant (arithmétique, logique, etc.) à une ou deux expressions constantes ; exemples : -1, 'A' - 'a', 2 * 3.14159265, "HELLO" + 6 ;
  • l'expression constituée par l'application de l'opérateur & (opérateur de calcul de l'adresse, voyez la section 2.2.10) à une variable statique, à un champ d'une variable statique de type structure ou à un élément d'un tableau statique dont le rang est donné par une expression constante ; exemples : &x, &fiche.nom, &table[50] ;
  • l'expression constituée par l'application de l'opérateur sizeof à un descripteur de type. Exemples : sizeof(int), sizeof(char *) ;
  • l'expression constituée par l'application de l'opérateur sizeof à une expression quelconque, qui ne sera pas évaluée ; exemples : sizeof x, sizeof(2 * x + 3).

Les expressions constantes peuvent être évaluées pendant la compilation. Cela est fait à titre facultatif par les compilateurs de certains langages. En C ce n'est pas facultatif : il est garanti que toute expression constante (et donc toute sous-expression constante d'une expression quelconque) sera effectivement évaluée avant que l'exécution ne commence. En termes de temps d'exécution, l'évaluation des expressions constantes est donc entièrement « gratuite ».

I-D. Types fondamentaux

Tab. 1 - Les types du langage C
Tab. 1 - Les types du langage C

Le tableau 1 présente l'ensemble des types connus du compilateur C. L'organisation générale de cet ensemble est évidente : on dispose de deux sortes de types de base, les nombres entiers et les nombres flottants, et d'une famille infinie de types dérivés obtenus en appliquant quelques procédés récursifs de construction soit à des types fondamentaux soit à des types dérivés définis de la même manière.

Cette organisation révèle un trait de l'esprit de C : le pragmatisme l'emporte sur l'esthétisme, parfois même sur la rigueur. Dans d'autres langages, les caractères, les booléens, les constantes symboliques, etc., sont codés de manière interne par des nombres, mais ce fait est officiellement ignoré par le programmeur, qui reste obligé de considérer ces données comme appartenant à des ensembles disjoints. En C on a fait le choix opposé, laissant au programmeur le soin de réaliser lui-même, à l'aide des seuls types numériques, l'implantation des types de niveau supérieur.

I-D-1. Nombres entiers et caractères

La classification des types numériques obéit à deux critères :

  • Si on cherche à représenter un ensemble de nombres tous positifs on pourra adopter un type non signé ; au contraire si on doit représenter un ensemble contenant des nombres positifs et des nombres négatifs on devra utiliser un type signé 5.
  • Le deuxième critère de classification des données numériques est la taille requise par leur représentation.

Comme précédemment, c'est un attribut d'un ensemble, et donc d'une variable devant représenter tout élément de l'ensemble, non d'une valeur particulière. Par exemple, le nombre 123 considéré comme un élément de l'ensemble {0 ... 65535} est plus encombrant que le même nombre 123 quand il est considéré comme un élément de l'ensemble {0 ... 255}.

Avec N chiffres binaires (ou bits) on peut représenter :

  • soit les 2N nombres positifs 0, 1, ... 2N - 1 (cas non signé) ;
  • soit les 2N nombres positifs et négatifs -2N-1, ... 2N-1 - 1 (cas signé).

De plus, la représentation signée et la représentation non signée des éléments communs aux deux domaines (les nombres 0, 1, ... 2N-1 - 1) coïncident.

Le type caractère. Un objet de type char peut être défini, au choix, comme :

  • un nombre entier pouvant représenter n'importe quel caractère du jeu de caractères de la machine utilisée ;
  • un nombre entier occupant la plus petite cellule de mémoire adressable séparément 6. Sur les machines

actuelles les plus répandues cela signifie généralement un octet (8 bits).

Le plus souvent, un char est un entier signé ; un unsigned char est alors un entier non signé. Lorsque les char sont par défaut non signés, la norme ANSI prévoit la possibilité de déclarer des signed char. On notera que la signification d'un char en C, un entier petit, est très différente de celle d'un char en Pascal (dans ce langage, l'ensemble des caractères et celui des nombres sont disjoints). En C, ch étant une variable de type char, rien ne s'oppose à l'écriture de l'expression ch - 'A' + 32 qui est tout à fait homogène, puisque entièrement faite de nombres.

Le caractère « impossible ». Toutes les valeurs qu'il est possible de ranger dans une variable de type char sont en principe des caractères légaux. Or la plupart des programmes qui lisent des caractères doivent être capables de manipuler une valeur supplémentaire, distincte de tous les « vrais » caractères, signifiant « la fin des données ». Pour cette raison, les variables et fonctions qui représentent ou renvoient des caractères sont souvent déclarées int, non char : n'importe quelle valeur appartenant au type int mais n'appartenant pas au type char peut alors servir d'indicateur de fin de données. Par exemple, une telle valeur est définie dans le fichier stdio.h, c'est la constante symbolique EOF.

Les entiers courts et longs. Il est garanti que toute donnée représentable dans le type short est représentable aussi dans le type long 7 (en bref : un long n'est pas plus court qu'un short !), mais la taille exacte des données de ces types n'est pas fixée par la norme du langage. De nos jours on trouve souvent :

  • unsigned short : 16 bits pour représenter un nombre entier compris entre 0 et 65.535
  • short : 16 bits pour représenter un nombre entier compris entre -32.768 et 32.767
  • unsigned long : 32 bits pour représenter un nombre entier entre 0 et 4.294.967.296
  • long : 32 bits pour représenter un entier entre -2.147.483.648 et 2.147.483.647

Le type int. En principe, le type int correspond à la taille d'entier la plus efficace, c'est-à-dire la plus adaptée à la machine utilisée. Sur certains systèmes et compilateurs int est synonyme de short, sur d'autres il est synonyme de long.

Le type int peut donc poser un problème de portabilité 8 : le même programme, compilé sur deux machines distinctes, peut avoir des comportements différents. D'où un conseil important : n'utilisez le type int que pour des variables locales destinées à contenir des valeurs raisonnablement petites (inférieures en valeur absolue à 32767) . Dans les autres cas il vaut mieux expliciter char, short ou long selon le besoin.

A propos des booléens. En C il n'existe donc pas de type booléen spécifique. Il faut savoir qu'à tout endroit ou une expression booléenne est requise (typiquement, dans des instructions comme if ou while) on peut faire figurer n'importe quelle expression ; elle sera tenue pour vraie si elle est non nulle, elle sera considérée fausse sinon. Ainsi, dans un contexte conditionnel,

Sélectionnez
expr

(c'est-à-dire expr « vraie ») équivaut à

Sélectionnez
expr != 0

(expr différente de 0). Inversement, lorsqu'un opérateur (égalité, comparaison, etc.) produit une valeur booléenne, il rend 0 pour faux et 1 pour vrai.

Signalons aux esthètes que le fichier <types.h> comporte les déclarations :

Sélectionnez

enum { false, true };
typedef unsigned char Boolean;

qui introduisent la constante false valant 0, la constante true valant 1 et le type Boolean comme le type le moins encombrant dans lequel on peut représenter ces deux valeurs.

5 On dit parfois qu'une donnée « est un entier signé » ou « est un entier non signé ». C'est un abus de langage : le caractère signé ou non signé n'est pas un attribut d'un nombre (un nombre donné est positif ou négatif, c'est tout) mais de l'ensemble de nombres qu'on a choisi de considérer et, par extension, de toute variable censée pouvoir représenter n'importe quelle valeur de cet ensemble.

6A retenir : un objet de type char est « unitaire » aussi bien du point de vue des tailles que de celui des adresses. Quelle que soit la machine utilisée, le compilateur C fera en sorte que le programmeur voie ces objets de la manière suivante : si t est un tableau de char, la taille (au sens de l'opérateur sizeof, cf. section II.B.11) de t[0] vaut une unité de taille, et l'écart entre les adresses de t[1] et t[0] vaut une unité d'adressage. On peut dire que ces propriétés définissent le type char (ou, si vous préférez, les unités de taille et d'adressage).

7Si on considère un type comme l'ensemble de ses valeurs, on a donc les inclusions larges char ⊆ short ⊆ long (et aussi float ⊆ double ⊆ long double).

8Un programme écrit pour une machine ou un système A est dit portable s'il suffit de le recompiler pour qu'il tourne correctement sur une machine différente B. Par exemple, « putchar('A') ; » est une manière portable d'obtenir l'affichage du caractère A, tandis que « putchar(65) ; » est (sur un système utilisant le code ASCII) une manière non portable d'obtenir le même affichage. Etre portable est un critère de qualité et de fiabilité important. On invoque l'efficacité pour justifier l'écriture de programmes non portables ; l'expérience prouve que, lorsque son écriture est possible, un programme portable est toujours meilleur qu'un programme non portable prétendu équivalent.

I-D-2. Types énumérés

Un type énuméré, ou énumération, est constitué par une famille finie de nombres entiers, chacun associé à un identificateur qui en est le nom. Mis à part ce qui touche à la syntaxe de leur déclaration, il n'y a pas grand-chose à dire à leur sujet. La syntaxe de la déclaration des énumérations est expliquée à la section 5.3. Par exemple, l'énoncé :

Sélectionnez

enum jour_semaine { lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche };

introduit un type énuméré, appelé enum jour semaine, constitué par les constantes lundi valant 0, mardi valant 1, mercredi valant 2, etc. Ainsi, les expressions mardi + 2 et jeudi représentent la même valeur. Les valeurs d'un type énuméré se comportent comme des constantes entières ; elles font donc double emploi avec celles qu'on définit à l'aide de #define (cf. section VIII.A.2). Leur unique avantage réside dans le fait que certains compilateurs détectent parfois, mais ce n'est pas exigé par la norme, les mélanges entre objets de types énumérés distincts ; ces types sont alors le moyen d'augmenter la sécurité des programmes. A propos des types énumérés voyez aussi la section 5.3

I-D-3. Nombres flottants

La norme ANSI prévoit trois types de nombres flottants : float (simple précision), double (double précision) et long double (précision étendue). La norme ne spécifie pas les caractéristiques de tous ces types. Il est garanti que toute valeur représentable dans le type float est représentable sans perte d'information dans le type double, et toute valeur représentable dans le type double l'est dans le type long double.

Typiquement, sur des systèmes de taille moyenne, un float occupe 32 bits et un double 64, ce qui donne par exemple des float allant de -1.70E38 à -0.29E-38 et de 0.29E-38 à 1.70E38 avec 7 chiffres décimaux significatifs, et des double allant de -0.90E308 à -0.56E-308 et de 0.56E-308 à 0.90E308 avec 15 chiffres décimaux significatifs.

Les long double correspondent généralement aux flottants de grande précision manipulés par certains coprocesseurs arithmétiques ou les bibliothèques de sous-programmes qui les simulent. Mais il n'est pas exclu que sur un système particulier un long double soit la même chose qu'un double.

I-E. Variables

I-E-1. Syntaxe des déclarations

La forme complète de la déclaration d'une variable sera expliquée à la section 5.4. Dans le cas le plus simple on trouve

spécification var-init , var-init , ... var-init ;

ou spécification est de la forme :

Image non disponible

et chaque var-init est de la forme :

Image non disponible

Exemples :

Sélectionnez

int x, y = 0, z;
extern float a, b;
static unsigned short cpt = 1000;

Les déclarations de variables peuvent se trouver :

  • en dehors de toute fonction, il s'agit alors de variables globales ;
  • à l'intérieur d'un bloc, il s'agit alors de variables locales ;
  • dans l'en-tête d'une fonction, il s'agit alors d'arguments formels, placés
    • soit dans les parenthèses de l'en-tête (fonction définie en syntaxe ANSI avec un prototype),
    • soit entre le nom de la fonction et le f initial (fonction définie en syntaxe originale ou sans prototype).

Exemple avec prototype :

Sélectionnez

long i = 1;
int une_fonction(int j) {
short k;
...
}

Exemple sans prototype :

Sélectionnez

long i = 1;
int une_fonction(j)
int j;
{
short k;
...
}

Ci-dessus, i est une variable globale, k une variable locale et j un argument formel de une fonction

I-E-2. Visibilité des variables

La question de la visibilité des identificateurs (c'est-à-dire « quels sont les identificateurs auxquels on peut faire référence en un point d'un programme ? ») est réglée en C comme dans la plupart des langages comportant la structure de bloc, avec une simplification : les fonctions ne peuvent pas être imbriquées les unes dans les autres, et une complication : tout bloc peut comporter ses propres définitions de variables locales.

Un bloc est une suite de déclarations et d'instructions encadrée par une accolade ouvrante "{" et l'accolade fermante "}" correspondante. Le corps d'une fonction est lui-même un bloc, mais d'autres blocs peuvent être imbriqués dans celui-là.

VARIABLES LOCALES. Tout bloc peut comporter un ensemble de déclarations de variables, qui sont alors dites locales au bloc en question. Une variable locale ne peut être référencée que depuis l'intérieur du bloc ou elle est définie ; en aucun cas on ne peut y faire référence depuis un point extérieur à ce bloc. Dans le bloc ou il est déclaré, le nom d'une variable locale masque toute variable de même nom définie dans un bloc englobant le bloc en question.

Toutes les déclarations de variables locales à un bloc doivent être écrites au début du bloc, avant la première instruction.

ARGUMENTS FORMEL. Pour ce qui concerne leur visibilité, les arguments formels des fonctions sont considérés comme des variables locales du niveau le plus haut, c'est-à-dire des variables déclarées au début du bloc le plus extérieur 9. Un argument formel est accessible de l'intérieur de la fonction, partout ou une variable locale plus profonde ne le masque pas. En aucun cas on ne peut y faire référence depuis l'extérieur de la fonction.

VARIABLES GLOBALES. Le nom d'une variable globale ou d'une fonction peut être utilisé depuis n'importe quel point compris entre sa déclaration (pour une fonction : la fin de la déclaration de son en-tête) et la fin du fichier ou la déclaration figure, sous réserve de ne pas être masquée par une variable locale ou un argument formel de même nom.

La question de la visibilité inter-fichiers est examinée à la section 1.6. On peut noter d'ores et déjà qu'elle ne se pose que pour les variables globales et les fonctions, et qu'elle concerne l'édition de liens, non la compilation, car le compilateur ne traduit qu'un fichier source à la fois et, pendant la traduction d'un fichier, il ne « voit » pas les autres.

9Par conséquent, on ne doit pas déclarer un argument formel et une variable locale du niveau le plus haut avec le même nom.

I-E-3. Allocation et durée de vie des variables

Les variables globales sont toujours statiques, c'est-à-dire permanentes : elles existent pendant toute la durée de l'exécution. Le système d'exploitation se charge, immédiatement avant l'activation du programme, de les allouer dans un espace mémoire de taille adéquate, éventuellement garni de valeurs initiales.

A l'opposé, les variables locales et les arguments formels des fonctions sont automatiques : l'espace correspondant est alloué lors de l'activation de la fonction ou du bloc en question et il est rendu au système lorsque le contrôle quitte cette fonction ou ce bloc. Certains qualifieurs (static, register, voir les sections 1.5.5 et 1.5.6) permettent de modifier l'allocation et la durée de vie des variables locales. Remarque. On note une grande similitude entre les variables locales et les arguments formels des fonctions : ils ont la même visibilité et la même durée de vie. En réalité c'est presque la même chose : les arguments formels sont de vraies variables locales avec l'unique particularité d'être automatiquement initialisés (par les valeurs des arguments effectifs) lors de l'activation de la fonction.

I-E-4. Initialisation des variables

Variables statiques. En toute circonstance la déclaration d'une variable statique peut indiquer une valeur initiale à ranger dans la variable. Cela est vrai y compris pour des variables de types complexes (tableaux ou structures). Exemple :

Sélectionnez

double x = 0.5e3;
int t[5] = { 11, 22, 33, 44, 55 };

Bien que la syntaxe soit analogue, une telle initialisation n'a rien en commun avec une affectation comme celles qui sont faites durant l'exécution du programme. Il s'agit ici uniquement de préciser la valeur qui doit être déposée dans l'espace alloué à la variable, avant que l'exécution ne commence.

Par conséquent :

  • la valeur initiale doit être définie par une expression constante (calculable durant la compilation) ;
  • une telle initialisation est entièrement gratuite, elle n'a aucune incidence ni sur la taille ni sur la durée du programme exécutable produit. Les variables statiques pour lesquelles aucune valeur initiale n'est indiquée sont remplies de zéros. L'interprétation de ces zéros dépend du type de la variable.

Variables automatiques. Les arguments formels des fonctions sont automatiquement initialisés lors de leur création (au moment de l'appel de la fonction) par les valeurs des arguments effectifs. Cela est la définition même des arguments des fonctions.

La déclaration d'une variable locale peut elle aussi comporter une initialisation. Mais il ne s'agit pas de la même sorte d'initialisation que pour les variables statiques : l'initialisation représente ici une affectation tout à fait ordinaire. Ainsi, placée à l'intérieur d'un bloc, la construction

Sélectionnez

    int i = exp; 	/* déclaration + initialisation */

équivaut au couple

Sélectionnez

    int i; 			/* déclaration */
    ...
    i = exp ; 		/* affectation */

Par conséquent :

  • l'expression qui donne la valeur initiale n'a pas à être constante, puisqu'elle est évaluée à l'exécution, chaque fois que la fonction ou le bloc est activé ;
  • une telle initialisation « coûte » le même prix que l'affectation correspondante, c'est-à-dire le temps d'évaluation de l'expression qui définit la valeur initiale.

Les variables automatiques pour lesquelles aucune valeur initiale n'est indiquée sont allouées avec une valeur imprévisible.

Remarque : dans le C original, une variable automatique ne peut être initialisée que si elle est simple (c'est-à-dire autre que tableau ou structure). Cette limitation ne fait pas partie du C ANSI.

I-E-5. Variables locales statiques

Le qualifieur static, placé devant la déclaration d'une variable locale, produit une variable qui est

  • pour sa visibilité, locale ;
  • pour sa durée de vie, statique (c'est-à-dire permanente).

Elle n'est accessible que depuis l'intérieur du bloc ou elle est déclarée, mais elle est créée au début de l'activation du programme et elle existe aussi longtemps que dure l'exécution de celui-ci. Exemple :

Sélectionnez

void bizarre1(void) {
    static int cpt = 1000;
    printf("%d ", cpt);
    cpt++;
}

Lorsque la déclaration d'une telle variable comporte une initialisation, il s'agit de l'initialisation d'une variable statique : elle est effectuée une seule fois avant l'activation du programme. D'autre part, une variable locale statique conserve sa valeur entre deux activations consécutives de la fonction. Ainsi, des appels successifs de la fonction ci-dessus produisent l'affichage des valeurs 1000, 1001, 1002, etc. On aurait pu obtenir un effet analogue avec le programme

Sélectionnez

int cpt = 1000;

void bizarre2(void) {
    printf("%d ", cpt);
    cpt++;
}

mais ici la variable cpt est globale et peut donc être modifiée inconsidérément par une autre fonction, ou entrer en conflit avec un autre objet de même nom, tandis que dans la première version elle n'est visible que depuis l'intérieur de la fonction et donc à l'abri des manipulations maladroites et des collisions de noms. On notera pour finir que la version suivante est erronée :

Sélectionnez

void bizarre3(void) {
    int cpt = 1000;
    printf("%d ", cpt);
    cpt++;
}

En effet, tous les appels de bizarre3 afficheront la même valeur 1000.

Attention. Malgré tout le bien qu'on vient d'en dire, les variables locales statiques ont une particularité potentiellement fort dangereuse : il en existe une seule instance pour toutes les activations de la fonction dans laquelle elles sont déclarées. Ainsi, dans l'exemple suivant :

Sélectionnez

void fonction_suspecte(void) {
    static int i;
    ...
    ff fonction_suspecte(); fi
    ...
}

la valeur de la variable i avant et après l'appel de fonction suspecte (c'est-à-dire aux points ff et fi) peut ne pas être la même, car la deuxième activation de fonction suspecte accède aussi à i. Cela est tout à fait inhabituel pour une variable locale. Conséquence à retenir : les variables locales statiques se marient mal avec la récursivité.

I-E-6. Variables critiques

Le qualifieur register précédant une déclaration de variable informe le compilateur que la variable en question est très fréquemment accédée pendant l'exécution du programme et qu'il y a donc lieu de prendre toutes les dispositions utiles pour en accélérer l'accès. Par exemple, dans certains calculateurs de telles variables sont logées dans un registre de l'unité centrale de traitement (CPU) plutôt que dans la mémoire centrale ; de cette manière l'accès à leur valeur ne met pas en œuvre le bus de la machine.

Les variables ainsi déclarées doivent être locales et d'un type simple (nombre, pointeur). Elles sont automatiquement initialisées à zéro chaque fois qu'elles sont créées. Le compilateur accorde ce traitement spécial aux variables dans l'ordre ou elles figurent dans les déclarations. Lorsque cela n'est plus possible (par exemple, parce que tous les registres de la CPU sont pris) les déclarations register restantes sont ignorées. Il convient donc d'appliquer ce qualifieur aux variables les plus critiques d'abord. Exemple :

Sélectionnez

char *strcpy(char *dest, char *srce) {
    register char *d = dest, *s = srce;
    while ((*d++ = *s++) != 0)
        ;
    return dest;
}

Attention. L'utilisation du qualifieur register est intéressante lorsque l'on doit utiliser un compilateur rustique, peu « optimisateur ». Or de nos jours les compilateurs de C ont fini par devenir très perfectionnés et intègrent des algorithmes d'optimisation, parmi lesquels la détermination des variables critiques et leur allocation dans les registres de la CPU. Il s'avère alors que le programmeur, en appliquant le qualifieur register à ses variables préférées (qu'il croit critiques alors qu'elles ne le sont pas réellement), gène le travail du compilateur et obtient un programme moins efficace que s'il n'avait jamais utilisé ce qualifieur.

I-E-7. Variables constantes et volatiles

Le qualifieur const placé devant une variable ou un argument formel informe le compilateur que la variable ou l'argument en question ne changera pas de valeur tout au long de l'exécution du programme ou de l'activation de la fonction. Ce renseignement permet au compilateur d'optimiser la gestion de la variable, la nature exacte d'une telle optimisation n'étant pas spécifiée. Par exemple un compilateur peut juger utile de ne pas allouer du tout une variable qualifiée const et de remplacer ses occurrences par la valeur initiale 10 indiquée lors de la déclaration. Il est conseillé de toujours déclarer const les variables et les arguments formels qui peuvent l'être.

Note. C'est regrettable mais, pour la plupart des compilateurs, une variable qualifiée const n'est pas tout à fait une expression constante au sens de la section 1.3.4. En particulier, pour ces compilateurs une variable, même qualifiée const, ne peut pas être utilisée pour indiquer le nombre d'éléments dans une déclaration de tableau.

Le C ANSI introduit aussi les notions de pointeur constant et de pointeur sur constante, expliquées à la section 5.4.2.

Le sens du qualifieur volatile dépend lui aussi de l'implémentation. Il diminue le nombre d'hypothèses, et donc d'optimisations, que le compilateur peut faire sur une variable ainsi qualifiée. Par exemple toute variable dont la valeur peut être modifiée de manière asynchrone (dans une fonction de détection d'interruption, ou par un canal d'entrée-sortie, etc.) doit être qualifiée volatile, sur les systèmes ou cela a un sens. Cela prévient le compilateur que sa valeur peut changer mystérieusement, y compris dans une section du programme qui ne comporte aucune référence à cette variable.

Les compilateurs sont tenus de signaler toute tentative décelable de modification d'une variable const. Mis à part cela, sur un système particulier ces deux qualifieurs peuvent n'avoir aucun autre effet. Ils n'appartiennent pas au C original.

10La déclaration d'une variable const doit nécessairement comporter une initialisation car sinon, une telle variable ne pouvant pas être affectée par la suite, elle n'aurait jamais de valeur définie.

I-F. Variables, fonctions et compilation séparée

I-F-1. Identificateurs publics et privés

Examinons maintenant les règles qui régissent la visibilité inter-fichiers des identificateurs. La question ne concerne que les noms de variables et de fonctions, car les autres identificateurs (noms de structures, de types, etc.) n'existent que pendant la compilation et ne peuvent pas être partagés par deux fichiers. Il n'y a pas de problème pour les variables locales, dont la visibilité se réduit à l'étendue de la fonction ou du bloc contenant leur définition. Il ne sera donc question que des noms des variables globales et des noms des fonctions.

Jargon. Identificateurs publics et privés. Un nom de variable ou de fonction défini dans un fichier source et pouvant être utilisé dans d'autres fichiers sources est dit public. Un identificateur qui n'est pas public est appelé privé.

Règle 1 :

  • Sauf indication contraire, tout identificateur global est public ;
  • le qualifieur static, précédant la déclaration d'un identificateur global, rend celui-ci privé.

On prendra garde au fait que le qualifieur static n'a pas le même effet quand il s'applique à un identificateur local (static change la durée de vie, d'automatique en statique, sans toucher à la visibilité) et quand il s'applique à un identificateur global (static change la visibilité, de publique en privée, sans modifier la durée de vie).

Lorsqu'un programme est décomposé en plusieurs fichiers sources il est fortement conseillé, pour ne pas dire obligatoire, d'utiliser le qualifieur static pour rendre privés tous les identificateurs qui peuvent l'être. Si on ne suit pas cette recommandation on verra des fichiers qui étaient corrects séparément devenir erronés lorsqu'ils sont reliés, uniquement parce qu'ils partagent à tort des identificateurs publics.

I-F-2. Déclaration d'objets externes

Nous ne considérons donc désormais que les noms publics. Un identificateur référencé dans un fichier alors qu'il est défini dans un autre fichier est appelé externe. En général, les noms externes doivent faire l'objet d'une déclaration : le compilateur ne traitant qu'un fichier à la fois, les propriétés de l'objet externe doivent être indiquées pour que la compilation puisse avoir lieu correctement.

Jargon :

Définition et déclaration d'une variable ou d'une fonction. Aussi bien une déclaration qu'une définition d'un nom de variable ou de fonction est une formule qui spécifie la classe syntaxique (variable ou fonction) et les attributs (type, valeur initiale, etc.) de l'identificateur en question. En plus de cela :

  • une définition produit la création de l'objet dont l'identificateur est le nom ;
  • une déclaration se limite à indiquer que l'objet en question a dû être créé dans un autre fichier qui sera fourni lors de l'édition de liens. (« Créer » une variable ou une fonction c'est réserver l'espace correspondant, rempli par l'éventuelle valeur initiale de la variable ou par le code de la fonction).

Règle 2 :

  • Toute variable doit avoir été définie (c'est-à-dire déclarée normalement) ou déclarée externe avant son utilisation ;
  • une fonction peut être référencée alors qu'elle n'a encore fait l'objet d'aucune définition ni déclaration externe ; elle est alors supposée être
    • externe,
    • à résultat entier (int),
    • sans prototype (cf. section IV.B) ;
  • par conséquent, si une fonction n'est pas à résultat entier alors elle doit être soit définie soit déclarée externe avant son appel, même si elle est ultérieurement définie dans le fichier ou figure l'appel.

La déclaration externe d'une variable s'obtient en faisant précéder une déclaration ordinaire du mot-clé extern. Exemple :

Sélectionnez

extern unsigned long n;

Dans un autre fichier cette variable aura été définie :

Sélectionnez

unsigned long n;

La déclaration externe d'une variable doit être identique, au mot extern près, à sa définition. Sauf pour les deux points suivants :

  • une déclaration externe ne doit pas comporter d'initialisateur (puisque la déclaration externe n'alloue pas la variable),
  • dans une déclaration externe de tableau, il est inutile d'indiquer la taille de celui-ci (puisque la déclaration externe n'alloue pas le tableau).

Exemple. Dans le fichier ou sont définies les variables n et table, on écrira :

Sélectionnez

unsigned long n = 1000;
int table[100];

Dans un autre fichier, ou ces variables sont uniquement référencées, on écrira :

Sélectionnez

extern unsigned long n;
extern int table[];

La déclaration externe d'une fonction s'obtient en écrivant l'en-tête de la fonction, précédé du mot extern et suivi d'un point-virgule ; le mot extern est facultatif. Exemple : définition de la fonction

Sélectionnez

double carre(double x) {
    return x * x;
}

Déclaration externe dans un autre fichier :

Sélectionnez

    double carre(double x);

ou

Sélectionnez

    double carre(double);

ou l'un ou l'autre de ces énoncés, précédé du mot extern.

En syntaxe originale (c'est-à-dire « sans prototype ») il faut en outre ne pas écrire les arguments formels.

Définition :

Sélectionnez

double carre(x)
    double x;
    {
    return x * x;
    }

Déclaration externe dans un autre fichier :

Sélectionnez

    double carre();

Règle 3 :

Dans l'ensemble des fichiers qui constituent un programme, chaque nom public :

  • doit faire l'objet d'une et une seule définition ;
  • peut être déclaré externe (y compris dans le fichier ou il est défini) un nombre quelconque de fois.

Cette règle volontariste est simple et elle exprime la meilleure fa»con de programmer. Il faut savoir cependant que chaque système tolère des écarts, qui révèlent surtout la rusticité de l'éditeur de liens sous-jacent. La clarté des concepts et la fiabilité des programmes y perdent beaucoup.

Un comportement fréquent est le suivant : appelons momentanément « déclaration-définition » une expression générale de la forme

Image non disponible

Nous pouvons donner la règle relâchée :

Règle 3 :

Dans l'ensemble des fichiers qui constituent un programme, chaque nom public peut faire l'objet d'un nombre quelconque de déclarations-définitions, mais :

  • il doit y avoir au moins une déclaration-définition sans le mot-clé extern ;
  • il peut y avoir au plus une déclaration-définition comportant un initialisateur.

Des techniques et conseils pour écrire des programmes modulaires en C sont exposés à la section 8.2.


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • II. Opérateurs et expressions
    • II-A. Généralités
      • II-A-1. Lvalue et rvalue
      • II-A-2. Priorité des opérateurs
    • II-B. Présentation détaillée des opérateurs
      • II-B-1. Appel de fonction ()
      • II-B-2. Indexation []
      • II-B-3. Sélection .
      • II-B-4. Sélection dans un objet pointé ->
      • II-B-5. Négation !
      • II-B-6. Complément à 1 ~
      • II-B-7. Les célèbres ++ et --
      • II-B-8. Moins unaire -
      • II-B-9. Indirection *
      • II-B-10. Obtention de l'adresse &
      • II-B-11. Opérateur sizeof
      • II-B-12. Conversion de type (\cast" operator)
      • II-B-13. Opérateurs arithmétiques
      • II-B-14. Décalages << >>
      • II-B-15. Comparaisons == != < <= > >=
      • II-B-16. Opérateurs de bits & | ^
      • II-B-17. Connecteurs logiques && et ||
      • II-B-18. Expression conditionnelle ? :
      • II-B-19. Affectation =
      • II-B-20. Autres opérateurs d'affectation += *= etc.
      • II-B-21. L'opérateur virgule ,
    • II-C. Autres remarques
      • II-C-1. Les conversions usuelles
      • II-C-2. L'ordre d'évaluation des expressions
      • II-C-3. Les opérations non abstraites

II. Opérateurs et expressions

II-A. Généralités

Dans cette section nous étudions les opérateurs et les expressions du langage C. Les expressions simples sont les constantes littérales (0, 'A', 0.31416e1, etc.), les constantes symboliques (lundi, false, etc.) et les noms de variables (x, nombre, etc.). Les opérateurs servent à construire des expressions complexes, comme 2 * x + 3 ou sin(0.31416e1). Les propriétés d'une expression complexe découlent essentiellement de la nature de l'opérateur qui chapeaute l'expression.

On peut distinguer les expressions pures des expressions avec effet de bord 11. Dans tous les cas une expression représente une valeur. Une expression pure ne fait que cela : l'état du système est le même avant et après son évaluation. Au contraire, une expression à effet de bord modifie le contenu d'une ou plusieurs variables. Par exemple, l'expression y + 1 est pure, tandis que l'affectation x = y + 1 (qui en C est une expression) est à effet de bord, car elle modifie la valeur de la variable x. Comme nous le verrons, en C un grand nombre d'expressions ont de tels effets.

Remarque 1. Les opérateurs dont il sera question ici peuvent aussi apparaitre dans les déclarations, pour la construction des types dérivés (tableaux, pointeurs et fonctions) comme dans la déclaration complexe :

Sélectionnez

char (*t[20])();

La signification des expressions ainsi écrites est alors très différente de celle des expressions figurant dans la partie exécutable des programmes, mais on peut signaler d'ores et déjà que toutes ces constructions obéissent aux mêmes règles de syntaxe, notamment pour ce qui concerne la priorité des opérateurs et l'usage des parenthèses. La question des déclarations complexes sera vue à la section 5.4.

Remarque 2. C'est une originalité de C que de considérer les « désignateurs » complexes (les objets pointés, les éléments des tableaux, les champs des structures, etc.) comme des expressions construites avec des opérateurs et obéissant à la loi commune, notamment pour ce qui est des priorités. On se souvient qu'en Pascal les signes qui permettent d'écrire de tels désignateurs (c'est-à-dire les sélecteurs [], ^ et .) n'ont pas statut d'opérateur. Il ne viendrait pas à l'idée d'un programmeur Pascal d'écrire 2 + (t[i]) afin de lever une quelconque ambiguïté sur la priorité de + par rapport à celle de [], alors qu'en C de telles expressions sont habituelles. Bien sûr, dans 2 + (t[i]) les parenthèses sont superflues, car la priorité de l'opérateur [] est supérieure à celle de +, mais ce n'est pas le cas dans (2 + t)[i], qui est une expression également légitime.

11Effet de bord est un barbarisme ayant pour origine l'expression anglaise side effect qu'on peut traduire par effet secondaire souvent un peu caché, parfois dangereux.

II-A-1. Lvalue et rvalue

Toute expression possède au moins deux attributs : un type et une valeur. Par exemple, si i est une variable entière valant 10, l'expression 2 * i + 3 possède le type entier et la valeur 23. Dans la partie exécutable des programmes on trouve deux sortes d'expressions :

  • Lvalue (expressions signifiant « le contenu de... »). Certaines expressions sont représentées par une formule qui détermine un emplacement dans la mémoire ; la valeur de l'expression est alors définie comme le contenu de cet emplacement. C'est le cas des noms des variables, des composantes des enregistrements et des tableaux, etc. Une lvalue possède trois attributs : une adresse, un type et une valeur. Exemples : x,
Sélectionnez

table[i], fiche.numero.
  • Rvalue (expressions signifiant « la valeur de... »). D'autres expressions ne sont pas associées à un emplacement de la mémoire : elles ont un type et une valeur, mais pas d'adresse. C'est le cas des constantes et des expressions définies comme le résultat d'une opération arithmétique. Une rvalue ne possède que deux attributs : type, valeur. Exemples : 12, 2 * i + 3.

Toute lvalue peut être vue comme une rvalue ; il suffit d'ignorer le contenant (adresse) pour ne voir que le contenu (type, valeur). La raison principale de la distinction entre lvalue et rvalue est la suivante : seule une lvalue peut figurer à gauche du signe = dans une affectation ; n'importe quelle rvalue peut apparaitre à droite. Cela justifie les appellations lvalue (« left value ») et rvalue (« right value »).

Les sections suivantes préciseront, pour chaque sorte d'expression complexe, s'il s'agit ou non d'une lvalue.

Pour les expressions simples, c'est-à-dire les constantes littérales et les identificateurs, la situation est la suivante :

  • sont des lvalue :
    • les noms des variables simples (nombres et pointeurs),
    • les noms des variables d'un type struct ou union
  • ne sont pas des lvalue :
    • les constantes,
    • les noms des variables de type tableau,
    • les noms des fonctions.

II-A-2. Priorité des opérateurs

C comporte de très nombreux opérateurs. La plupart des caractères spéciaux désignent des opérations et subissent les mêmes règles syntaxiques, notamment pour ce qui concerne le jeu des priorités et la possibilité de parenthésage. La table 2 montre l'ensemble de tous les opérateurs, classés par ordre de priorité.

Image non disponible

Remarque. Dans certains cas un même signe, comme -, désigne deux opérateurs, l'un unaire (à un argument), l'autre binaire (à deux arguments). Dans le tableau 2, les suffixes un et bin précisent de quel opérateur il s'agit.

Le signe -> indique l'associativité « de gauche à droite » ; par exemple, l'expression x - y - z signifie (x - y) - z. Le signe <- indique l'associativité de « droite à gauche » ; par exemple, l'expression x = y = z signifie x = (y = z).

Notez que le sens de l'associativité des opérateurs précise la signification d'une expression, mais en aucun cas la chronologie de l'évaluation des sous-expressions.

II-B. Présentation détaillée des opérateurs

II-B-1. Appel de fonction ()

Opération : application d'une fonction à une liste de valeurs. Format :

Sélectionnez

expn ( exp1 , ... expn )

exp1 , ... expn sont appelés les arguments effectifs de l'appel de la fonction. exp0 doit être de type fonction rendant une valeur de type T ou bien 12 adresse d'une fonction rendant une valeur de type T. Alors exp0 ( exp1 , ... expn ) possède le type T. La valeur de cette expression découle de la définition de la fonction (à l'intérieur de la fonction, cette valeur est précisée par une ou plusieurs instructions « return exp ; »).

L'expression exp0 ( exp1 , ... expn ) n'est pas une lvalue.

Exemple :

Sélectionnez

y = carre(2 * x) + 3;

sachant que carre est le nom d'une fonction rendant un double, l'expression carre(2 * x) a le type double. Un coup d'œil au corps de la fonction (cf. section I.F.2) pourrait nous apprendre que la valeur de cette expression n'est autre que le carré de son argument, soit ici la valeur 4x2.

Les contraintes supportées par les arguments effectifs ne sont pas les mêmes dans le C ANSI et dans le C original (ceci est certainement la plus grande différence entre les deux versions du langage) :

A. En C ANSI, si la fonction a été définie ou déclarée avec prototype (cf. section IV.A) :

  • le nombre des arguments effectifs doit correspondre à celui des arguments formels 13 ;
  • chaque argument effectif doit être compatible, au sens de l'affectation, avec l'argument formel correspondant ;
  • la valeur de chaque argument effectif subit éventuellement les mêmes conversions qu'elle subirait dans l'affectation :
Sélectionnez

argument formel = argument effectif

B. En C original, ou en C ANSI si la fonction n'a pas été définie ou déclarée avec prototype :

  • aucune contrainte (de nombre ou de type) n'est imposée aux arguments effectifs ;
  • tout argument effectif de type char ou short est converti en int ;
  • tout argument effectif de type float est converti en double ;
  • les autres arguments effectifs ne subissent aucune conversion.

Par exemple, avec la fonction carre définie à la section 1.6.2, l'appel

Sélectionnez

    y = carre(2);

est erroné en C original (la fonction reçoit la représentation interne de la valeur entière 2 en « croyant » que c'est la représentation interne d'un double) mais il est correct en C ANSI (l'entier 2 est converti en double au moment de l'appel).

Toutes ces questions sont reprises plus en détail à la section 4.

Remarque 1. Bien noter que les parenthèses doivent apparaitre, même lorsque la liste des arguments est vide. Leur absence ne provoque pas d'erreur, mais change complètement le sens de l'expression. Cela constitue un piège assez vicieux tendu aux programmeurs dont la langue maternelle est Pascal.

Remarque 2. Il faut savoir que lorsqu'une fonction a été déclarée comme ayant des arguments en nombre variable (cf. section IV.C.4) les arguments correspondant à la partie variable sont traités comme les arguments des fonctions sans prototype, c'est-à-dire selon les règles de la section B ci-dessus.

Cette remarque est loin d'être marginale car elle concerne, excusez du peu, les deux fonctions les plus utilisées : printf et scanf. Ainsi, quand le compilateur rencontre l'instruction

Sélectionnez

printf(expr0, expr1, ... exprk);

il vérifie que expr0 est bien de type char *, mais les autres paramètres effectifs expr1, ... exprk sont traités selon les règles de la section B.

12 Cette double possibilité est commentée à la section 6.3.4.

13 Il existe néanmoins un moyen pour écrire des fonctions avec un nombre variable d'arguments (cf. section IV.C.4)

II-B-2. Indexation []

Définition restreinte (élément d'un tableau). Opération : accès au ieme élément d'un tableau. Format :

Sélectionnez

    exp0 [ exp1 ]

exp0 doit être de type « tableau d'objets de type T », exp1 doit être d'un type entier. Alors exp0[exp1] est de type T ; cette expression désigne l'élément du tableau dont l'indice est donné par la valeur de exp1. Deux détails auxquels on tient beaucoup en C :

  • le premier élément d'un tableau a toujours l'indice 0 ;
  • il n'est jamais vérifié que la valeur de l'indice dans une référence à un tableau appartient à l'intervalle 0 ... N-1 déterminé par le nombre N d'éléments alloués par la déclaration du tableau. Autrement dit, il n'y a jamais de « test de débordement ».

Exemple. L'expression t[0] désigne le premier élément du tableau t, t[1] le second, etc.

En C, les tableaux sont toujours à un seul indice ; mais leurs composantes peuvent être à leur tour des tableaux. Par exemple, un élément d'une matrice rectangulaire sera noté :

Sélectionnez

m[i][j]

Une telle expression suppose que m[i] est de type « tableau d'objets de type T » et donc que m est de type « tableau de tableaux d'objets de type T ». C'est le cas, par exemple, si m a été déclarée par une expression de la forme (NL et NC sont des constantes) :

Sélectionnez

double m[NL][NC];

Définition complète (indexation au sens large). Opération : accès à un objet dont l'adresse est donnée par une adresse de base et un déplacement. Format :

Sélectionnez

exp0 [ exp1 ]

exp0 doit être de type « adresse d'un objet de type T », exp1 doit être de type entier. Alors exp0[exp1] désigne l'objet de type T ayant pour adresse (voir la figure 2) :

Sélectionnez

valeur(exp0) + valeur(exp1) * taille(T)
Fig. 2 - L'indexation
Fig. 2 - L'indexation

Il est clair que, si exp0 est de type tableau, les deux définitions de l'indexation données coïncident. Exemple : si t est un tableau d'entiers et p un « pointeur vers entier » auquel on a affecté l'adresse de t[0], alors les expressions suivantes désignent le même objet :

Sélectionnez

t[i] p[i] *(p + i) *(t + i)

Dans un cas comme dans l'autre, l'expression exp0[exp1] est une lvalue, sauf si elle est de type tableau.

II-B-3. Sélection .

Opération : accès à un champ d'une structure ou d'une union. Format :

Sélectionnez

exp . identif

exp doit posséder un type struct ou union, et identif doit être le nom d'un des champs de la structure ou de l'union en question. En outre, exp doit être une lvalue. Alors, exp.identif désigne le champ identif de l'objet désigne par exp.

Cette expression est une lvalue, sauf si elle est de type tableau.

Exemple. Avec la déclaration

Sélectionnez

struct personne {
    long int num;
    struct {
        char rue[32];
        char *ville;
    } adresse;
} fiche;

les expressions

Sélectionnez

fiche.num 	fiche.adresse 	fiche.adresse.rue 	etc.

désignent les divers champs de la variable fiche. Seules les deux premières sont des lvalue.

II-B-4. Sélection dans un objet pointé ->

Opération : accès au champ d'une structure ou d'une union pointée. Format :

Sélectionnez

    exp->identif

exp doit posséder le type « adresse d'une structure ou d'une union » et identif doit être le nom d'un des champs de la structure ou de l'union en question. Dans ces conditions, exp->identif désigne le champ identif de la structure ou de l'union dont l'adresse est indiquée par la valeur de exp. Cette expression est une lvalue, sauf si elle est de type tableau.

Ainsi, exp->identif est strictement synonyme de (*exp).identif (remarquez que les parenthèses sont indispensables).

Par exemple, avec la déclaration :

Sélectionnez

struct noeud {
    int info;
    struct noeud *fils, *frere;
} *ptr;

les expressions suivantes sont correctes :

Sélectionnez

ptr->info ptr->fils->frere ptr->fils->frere->frere

II-B-5. Négation !

Opération : négation logique. Format :

Sélectionnez

!exp

Aucune contrainte. Cette expression désigne une valeur de l'ensemble {0,1}, définie par :

Image non disponible

Cette expression n'est pas une lvalue.

Remarque. Bien que cela ne soit pas exigé par le langage C, on évitera de « nier » (et plus généralement de comparer à zéro) des expressions d'un type flottant (float, double). A cause de l'imprécision inhérente à la plupart des calculs avec de tels nombres, l'égalité à zéro d'un flottant n'est souvent que le fruit du hasard.

II-B-6. Complément à 1 ~

Opération : négation bit à bit. Format :

Sélectionnez

        ~exp

exp doit être d'un type entier. Cette expression désigne l'objet de même type que exp qui a pour codage interne la configuration de bits obtenue en inversant chaque bit du codage interne de la valeur de exp : 1 devient 0, 0 devient 1.

Cette expression n'est pas une lvalue.

Remarque. Le complément à un n'est pas une opération abstraite (cf. section II.C.3). La portabilité d'un programme ou cet opérateur figure n'est donc pas assurée.

II-B-7. Les célèbres ++ et --

Il existe deux opérateurs unaires ++ différents : l'un est postfixé (écrit derrière l'opérande), l'autre préfixé (écrit devant).

1. Opération : post-incrémentation. Format :

Sélectionnez

exp++

exp doit être de type numérique (entier ou flottant) ou pointeur. Ce doit être une lvalue. Cette expression est caractérisée par :

  • un type : celui de exp ;
  • une valeur : la même que exp avant l'évaluation de exp++ ;
  • un effet de bord : le même que celui de l'affectation exp = exp + 1.

2. Opération : pré-incrémentation. Format :

Sélectionnez

++exp

exp doit être de type numérique (entier ou flottant) ou pointeur. Ce doit être une lvalue. Cette expression est caractérisée par :

  • un type : celui de exp ;
  • une valeur : la même que exp après l'évaluation de exp++ ;
  • un effet de bord : le même que celui de l'affectation exp = exp + 1.

Les expressions exp++ et ++exp ne sont pas des lvalue.

Exemple.

L'affectation équivaut à

Sélectionnez

    y = x++ ; 	y = x ; x = x + 1 ;
    y = ++x ; 	x = x + 1 ; y = x ;

L'opérateur ++ bénéficie de l'arithmétique des adresses au même titre que +. Ainsi, si exp est de type « pointeur vers un objet de type T », la quantité effectivement ajoutée à exp par l'expression exp++ dépend de la taille de T.

Il existe de même deux opérateurs unaires -- donnant lieu à des expressions exp-- et --exp. L'explication est la même, en remplaçant +1 par ¡1.

Application : réalisation d'une pile. Il est très agréable de constater que les opérateurs ++ et -- et la manière d'indexer les tableaux en C se combinent harmonieusement et permettent par exemple la réalisation simple et efficace de piles par des tableaux, selon le schéma suivant :

  • déclaration et initialisation d'une pile (OBJET est un type prédéfini, dépendant du problème particulier considéré ; MAXPILE est une constante représentant un majorant du nombre d'éléments dans la pile) :
Sélectionnez

    OBJET espace[MAXPILE];
    int nombreElements = 0;
  • opération « empiler la valeur de x » :
Sélectionnez

    if (nombreElements >= MAXPILE)
        erreur("tentative d'empilement dans une pile pleine");
    espace[nombreElements++] = x;
  • opération « dépiler une valeur et la ranger dans x » :
Sélectionnez

    if (nombreElements <= 0)
        erreur("tentative de depilement d'une pile vide");
    x = espace[--nombreElements];

On notera que, si on procède comme indiqué ci-dessus, la variable nombreElements possède constamment la valeur que son nom suggère : le nombre d'éléments effectivement présents dans la pile.

II-B-8. Moins unaire -

Opération : changement de signe. Format :

Sélectionnez

                        -exp

exp doit être une expression numérique (entière ou réelle). Cette expression représente l'objet de même type que exp dont la valeur est l'opposée de celle de exp. Ce n'est pas une lvalue.

II-B-9. Indirection *

Opération : accès à un objet pointé. On dit aussi « déréference ». Format :

Sélectionnez

        *exp

exp doit être une expression de type « adresse d'un objet de type T ». *exp représente alors l'objet de type T ayant pour adresse la valeur de exp.

L'expression *exp est une lvalue.

Remarque 1. On prendra garde au fait qu'il existe un bon nombre d'opérateurs ayant une priorité supérieure à celle de *, ce qui oblige souvent à utiliser des parenthèses. Ainsi par exemple les expressions Pascal e"[i] et e[i]" doivent respectivement s'écrire, en C, (*e)[i] et *(e[i]), la deuxième pouvant aussi s'écrire *e[i].

Remarque 2. L'expression *p signifie accès à la mémoire dont p contient l'adresse. C'est une opération « sans filet ». Quel que soit le contenu de p, la machine pourra toujours le considérer comme une adresse et accéder à la mémoire correspondante. Il appartient au programmeur de prouver que cette mémoire a été effectivement allouée au programme en question. Si c'est le cas on dit que la valeur de p est une adresse valide ; dans le cas contraire, les pires erreurs sont à craindre à plus ou moins court terme.

II-B-10. Obtention de l'adresse &

Opération : obtention de l'adresse d'un objet occupant un emplacement de la mémoire. Format :

Sélectionnez

&exp

exp doit être une expression d'un type quelconque T. Ce doit être une lvalue.

L'expression &exp a pour type « adresse d'un objet de type T » et pour valeur l'adresse de l'objet représenté par exp. L'expression &exp n'est pas une lvalue.

Ainsi, si i est une variable de type int et p une variable de type « pointeur vers un int », alors à la suite de l'instruction

Sélectionnez

p = &i;

i et *p désignent le même objet.

Exemple 1. Une utilisation fréquente de cet opérateur est l'obtention de l'adresse d'une variable en vue de la passer à une fonction pour qu'elle modifie la variable :

Sélectionnez

scanf("%d%lf%s", &i, &t[j], &p->nom);

Exemple 2. Une autre utilisation élégante de cet opérateur est la création de composantes « fixes » dans les structures chainées. Le programme suivant déclare une liste chainée circulaire représentée par le pointeur entree et réduite pour commencer à un unique maillon qui est son propre successeur (voir figure 3) ; d'autres maillons seront créés dynamiquement durant l'exécution :

Fig. 3 - Maillon fixe en tête d'une liste chainée
Fig. 3 - Maillon fixe en tête d'une liste chainée
Sélectionnez

struct en_tete {
    long taille;
    struct en_tete *suivant;
} en_tete_fixe = { 0, &en_tete_fixe };

struct en_tete *entree = &en_tete_fixe;

Remarque. Une expression réduite à un nom de tableau, à un nom de fonction ou à une constante chaine de caractères est considérée comme une constante de type adresse ; l'opérateur & appliqué à de telles expressions est donc sans objet. Un tel emploi de & devrait être considéré comme erroné, mais beaucoup de compilateurs se contentent de l'ignorer.

II-B-11. Opérateur sizeof

Opération : calcul de la taille correspondant à un type. Première forme :

Sélectionnez

    sizeof ( descripteur-de-type )

Cette expression représente un nombre entier qui exprime la taille qu'occuperait en mémoire un objet possédant le type indiqué (les descripteurs de types sont expliqués à la section 5.4).

Deuxième forme :

Sélectionnez

    sizeof exp

exp est une expression quelconque (qui n'est pas évaluée par cette évaluation de sizeof). L'expression sizeof

exp représente la taille qu'occuperait en mémoire un objet possédant le même type que exp. Dans un cas comme dans l'autre sizeof... est une expression constante. En particulier, ce n'est pas une lvalue.

La taille des objets est exprimée en nombre d'octets. Plus exactement, l'unité choisie est telle que la valeur de sizeof(char) soit 1 (on peut aussi voir cela comme une définition du type char).

Remarque 1. Dans le C original, le type de l'expression sizeof... est int. Dans le C ANSI, ce type peut changer d'un système à un autre (int, long, unsigned, etc.). Pour cette raison il est défini, sous l'appellation size t, dans le fichier stddef.h. Il est recommandé de déclarer de type size t toute variable (resp. toute fonction) devant contenir (resp. devant renvoyer) des nombres qui représentent des tailles.

Remarque 2. Lorsque son opérande est un tableau (ou une expression de type tableau) la valeur rendue par sizeof est l'encombrement effectif du tableau. Par exemple, avec la déclaration

Sélectionnez

    char tampon[80];

l'expression sizeof tampon vaut 80, même si cela parait en contradiction avec le fait que tampon peut être vu comme de type « adresse d'un char ». Il en découle une propriété bien utile : quel que soit le type du tableau t, la formule

Sélectionnez

    sizeof t / sizeof t[0]

exprime toujours le nombre d'éléments de t.

II-B-12. Conversion de type (\cast" operator)

Opération : conversion du type d'une expression. Format :

Sélectionnez

                ( type2 ) exp

type2 représente un descripteur de type ; exp est une expression quelconque. L'expression ci-dessus désigne un élément de type type2 qui est le résultat de la conversion vers ce type de l'élément représenté par exp.

L'expression (type2)exp n'est pas une lvalue.

Les conversions légitimes (et utiles) sont 14 :

  • Entier vers un entier plus long (ex. char → int). Le codage de exp est étendu de telle manière que la valeur représentée soit inchangée.
  • Entier vers un entier plus court (ex. long → short). Si la valeur de exp est assez petite pour être représentable dans le type de destination, sa valeur est la même après conversion. Sinon, la valeur de exp est purement et simplement tronquée (une telle troncation, sans signification abstraite, est rarement utile).
  • Entier signé vers entier non signé, ou le contraire. C'est une conversion sans travail : le compilateur se borne à interpréter autrement la valeur de exp, sans effectuer aucune transformation de son codage interne.
  • Flottant vers entier : la partie fractionnaire de la valeur de exp est supprimée. Par exemple, le flottant 3.14 devient l'entier 3. Attention, on peut voir cette conversion comme une réalisation de la fonction mathématique partie entière, mais uniquement pour les nombres positifs : la conversion en entier de -3.14 donne -3, non -4.
  • Entier vers flottant. Sauf cas de débordement (le résultat est alors imprévisible), le flottant obtenu est celui qui approche le mieux l'entier initial. Par exemple, l'entier 123 devient le flottant 123.0.
  • Adresse d'un objet de type T1 vers adresse d'un objet de type T2. C'est une conversion sans travail : le compilateur donne une autre interprétation de la valeur de exp, sans effectuer aucune transformation de son codage interne.

Danger ! Une telle conversion est entièrement placée sous la responsabilité du programmeur, le compilateur l'accepte toujours.

  • Entier vers adresse d'un objet de type T. C'est encore une conversion sans travail : la valeur de exp est interprétée comme un pointeur, sans transformation de son codage interne.

Danger ! Une telle conversion est entièrement placée sous la responsabilité du programmeur. De plus, si la représentation interne de exp n'a pas la taille voulue pour un pointeur, le résultat est imprévisible.
Toutes les conversions ou l'un des types en présence, ou les deux, sont des types struct ou union sont interdites.

Note 1. Si type2 et le type de exp sont numériques, alors la conversion effectuée à l'occasion de l'évaluation de l'expression ( type2 ) exp est la même que celle qui est faite lors d'une affectation

Sélectionnez

    x = expr;

ou x représente une variable de type type2.

Note 2. En toute rigueur, le fait que l'expression donnée par un opérateur de conversion de type ne soit pas une lvalue interdit des expressions qui auraient pourtant été pratiques, comme

Sélectionnez

    ((int) x)++; /* DANGER ! */

Cependant, certains compilateurs acceptent cette expression, la traitant comme

Sélectionnez

    x = (le type de x)((int) x + 1) ;

Exemple 1. Si i et j sont deux variables entières, l'expression i / j représente leur division entière (ou euclidienne, ou encore leur quotient par défaut). Voici deux manières de ranger dans x (une variable de type float) leur quotient décimal :

Sélectionnez

    x = (float) i / j; x = i / (float) j;

Et voici deux manières de se tromper (en n'obtenant que le résultat de la conversion vers le type float de leur division entière)

Sélectionnez

    x = i / j; /* ERREUR */ x = (float)(i / j); /* ERREUR */

Exemple 2. Une utilisation pointue et dangereuse, mais parfois nécessaire, de l'opérateur de conversion de type entre types pointeurs consiste à s'en servir pour « voir » un espace mémoire donné par son adresse comme possédant une certaine structure alors qu'en fait il n'a pas été ainsi déclaré :

  • déclaration d'une structure :
Sélectionnez

struct en_tete {
    long taille;
    struct en_tete *suivant;
};
  • déclaration d'un pointeur « générique » :
Sélectionnez

void *ptr;
  • imaginez que {pour des raisons non détaillées ici{ ptr possède à un endroit donné une valeur qu'il est légitime de considérer comme l'adresse d'un objet de type struct en tete (alors que ptr n'a pas ce type-là). Voici un exemple de manipulation cet objet :
Sélectionnez

    ((struct en_tete *) ptr)->taille = n;

Bien entendu, une telle conversion de type est faite sous la responsabilité du programmeur, seul capable de garantir qu'à tel moment de l'exécution du programme ptr pointe bien un objet de type struct en tete.

N.B. L'appel d'une fonction bien écrite ne requiert jamais un « cast ». L'opérateur de change- ment de type est parfois nécessaire, mais son utilisation diminue toujours la qualité du programme qui l'emploie, pour une raison facile à comprendre : cet opérateur fait taire le compilateur. En effet, si expr est d'un type pointeur et type2 est un autre type pointeur, l'expression (type2)expr est toujours acceptée par le compilateur sans le moindre avertissement. C'est donc une manière de cacher des erreurs sans les résoudre.

Exemple typique : si on a oublié de mettre en tête du programme la directive #include <stdlib.h>, l'utilisation de la fonction malloc de la bibliothèque standard soulève des critiques :

Sélectionnez

    ...
    MACHIN *p;
    ...
    p = malloc(sizeof(MACHIN));
    ...

A la compilation on a des avertissements. Par exemple (avec gcc) :

Sélectionnez

    monProgramme.c: In function `main'
    monProgramme.c:9:warning: assignment makes pointer from integer without a cast

On croit résoudre le problème en utilisant l'opérateur de changement de type

Sélectionnez

    ...
    p = (MACHIN *) malloc(sizeof(MACHIN)); /* CECI NE REGLE RIEN! */
    ...

et il est vrai que la compilation a lieu maintenant en silence, mais le problème n'est pas résolu, il est seulement caché. Sur un ordinateur ou les entiers et les pointeurs ont la même taille cela peut marcher, mais ailleurs la valeur rendue par malloc sera endommagée lors de son affectation à p. A ce sujet, voyez la remarque de la page 65.

Il suffit pourtant de bien lire l'avertissement affiché par le compilateur pour trouver la solution. L'affectation p = malloc(...) lui fait dire qu'on fabrique un pointeur (p) à partir d'un entier (le résultat de malloc) sans opérateur cast. Ce qui est anormal n'est pas l'absence de cast, mais le fait que le résultat de malloc soit tenu pour un entier 15, et il n'y a aucun moyen de rendre cette affectation juste aussi longtemps que le compilateur fera une telle hypothèse fausse. La solution est donc d'informer ce dernier à propos du type de malloc, en faisant précéder l'affectation litigieuse soit d'une déclaration de cette fonction

Sélectionnez

    void *malloc (size_t);

soit, c'est mieux, d'une directive ad hoc :

Sélectionnez

    #include <stdlib.h>
    ...
    p = malloc(sizeof(MACHIN));
    ...

14 Attention, il y a quelque chose de trompeur dans la phrase « conversion de exp ». N'oubliez pas que, contrairement à ce que suggère une certaine manière de parler, l'évaluation de l'expression (type)exp ne change ni le type ni la valeur de exp.

15 Rappelez-vous que toute fonction appelée et non déclarée est supposée de type int.

II-B-13. Opérateurs arithmétiques

Ce sont les opérations arithmétiques classiques : addition, soustraction, multiplication, division et modulo (reste de la division entière). Format :

Image non disponible

Aucune de ces expressions n'est une lvalue.

Avant l'évaluation de l'expression, les opérandes subissent les conversions « usuelles » (cf. section II.C.1). Le langage C supporte l'arithmétique des adresses (cf. section VI.B.1).

A propos de la division. En C on note par le même signe la division entière, qui prend deux opérandes entiers (short, int, long, etc.) et donne un résultat entier, et la division flottante dans laquelle le résultat est flottant (float, double). D'autre part, les règles qui commandent les types des opérandes et du résultat des expressions arithmétiques (cf. section II.C.1), et notamment la règle dite « du plus fort », ont la conséquence importante suivante : dans l'expression

Sélectionnez

    expr1 / expr2
  • si expr1 et expr2 sont toutes deux entières alors « / » est traduit par l'opération « division entière » 16, et le résultat est entier,
  • si au moins une des expressions expr1 ou expr2 n'est pas entière, alors l'opération faite est la division flottante des valeurs de expr1 et expr2 toutes deux converties dans le type double. Le résultat est une valeur double qui approche le rationnel expr1
Sélectionnez

expr2

du mieux que le permet la la précision du type double. Il résulte de cette règle un piège auquel il faut faire attention : 1/2 ne vaut pas 0:5 mais 0. De même, dans

Sélectionnez

    int somme, nombre;
    float moyenne;
    ...
    moyenne = somme / 100;

la valeur de moyenne n'est pas ce que ce nom suggère, car somme et 100 sont tous deux entiers et l'expression somme / 100 est entière, donc tronquée (et il ne faut pas croire que l'affectation ultérieure à la variable flottante moyenne pourra retrouver les décimales perdues). Dans ce cas particulier, la solution est simple :

Sélectionnez

    moyenne = somme / 100.0;

Même problème si le dénominateur avait été une variable entière, comme dans

Sélectionnez

    moyenne = somme / nombre;

ici la solution est un peu plus compliquée :

Sélectionnez

    moyenne = somme / (double) nombre;

16On prendra garde au fait que si les opérandes ne sont pas tous deux positifs la division entière du langage C ne coïncide pas avec le « quotient par défaut » (quotient de la « division euclidienne » des matheux). Ici, si q est le quotient de a par b alors |q| est le quotient de |a| par |b|. Par exemple, la valeur de (-17)/5 ou de 17/(-5) est -3 alors que le quotient par défaut de -17 par 5 est plutôt -4 (-17 = 5 * (-4) + 3).

II-B-14. Décalages << >>

Opération : décalages de bits. Format :

Image non disponible

exp1 et exp2 doivent être d'un type entier (char, short, long, int...). L'expression exp1 << exp2 (resp. exp1 >> exp2) représente l'objet de même type que exp1 dont la représentation interne est obtenue en décalant les bits de la représentation interne de exp1 vers la gauche 17 (resp. vers la droite) d'un nombre de positions égal à la valeur de exp2. Autrement dit,

Sélectionnez

    exp1 << exp2

est la même chose (si << 1 apparait exp2 fois) que

Sélectionnez

((exp1 << 1) << 1) ... << 1

Remarque analogue pour le décalage à droite >>.

Les bits sortants sont perdus. Les bits entrants sont :
  • dans le cas du décalage à gauche, des zéros ;
  • dans le cas du décalage à droite : si exp1 est d'un type non signé, des zéros ; si exp1 est d'un type signé, des copies du bit de signe 18.

Par exemple, si on suppose que la valeur de exp1 est codée sur huit bits, notés

Sélectionnez

b7b6b5b4b3b2b1b0

(chaque bi vaut 0 ou 1), alors

Sélectionnez

exp1 << 1 	= 	b6b5b4b3b2b1b00
exp1 >> 1 	= 	0b7b6b5b4b3b2b1 (cas non signé)
exp1 >> 1 	= 	b7b7b6b5b4b3b2b1 (cas signé)

Avant l'évaluation du résultat, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces expressions ne sont pas des lvalue.

Remarque. Les opérateurs de décalage de bits ne sont pas des opérations abstraites (cf. section II.C.3). La portabilité d'un programme ou ils figurent n'est donc pas assurée.

17Par convention la droite d'un nombre codé en binaire est le coté des bits de poids faible (les unités) tandis que la gauche est celui des bits de poids forts (correspondant aux puissances 2k avec k grand).

18De cette manière le décalage à gauche correspond (sauf débordement) à la multiplication par 2exp2 , tandis que le décalage à droite correspond à la division entière par 2exp2 , aussi bien si exp1 est signée que si elle est non signée

II-B-15. Comparaisons == != < <= > >=

Il s'agit des comparaisons usuelles : égal, différent, inférieur, inférieur ou égal, supérieur, supérieur ou égal. Format :

Image non disponible

exp1 et exp2 doivent être d'un type simple (nombre ou pointeur). Cette expression représente l'un des éléments (de type int) 1 ou 0 selon que la relation indiquée est ou non vérifiée par les valeurs de exp1 et exp2. Avant la prise en compte de l'opérateur, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces expressions ne sont pas des lvalue.

Notez que, contrairement à ce qui se passe en Pascal, ces opérateurs ont une priorité supérieure à celle des connecteurs logiques, ce qui évite beaucoup de parenthèses disgracieuses. Ainsi, en C, l'expression

Sélectionnez

        0 <= x && x < 10

est correctement écrite.

A propos de « être vrai » et « être non nul ». Comme on a dit (cf. section I.D.1), le type booléen n'existe pas en C. N'importe quelle expression peut occuper la place d'une condition ; elle sera tenue pour fausse si elle est nulle, pour vraie dans tous les autres cas. Une conséquence de ce fait est la suivante : à la place de

Sélectionnez

    if (i != 0) etc.
    while (*ptchar != '\0') etc.
    for (p = liste; p != NULL; p = p->suiv) etc.

on peut écrire respectivement

Sélectionnez

    if (i) etc.
    while (*ptchar) etc.
    for (p = liste; p; p = p->suiv) etc.

On admet généralement qu'à cause de propriétés techniques des processeurs, ces expressions raccourcies représentent une certaine optimisation des programmes. C'est pourquoi les premiers programmeurs C, qui ne disposaient que de compilateurs simples, ont pris l'habitude de les utiliser largement. On ne peut plus aujourd'hui conseiller cette pratique. En effet, les compilateurs actuels sont suffisamment perfectionnés pour effectuer spontanément cette sorte d'optimisations et il n'y a plus aucune justification de l'emploi de ces comparaisons implicites qui rendent les programmes bien moins expressifs.

Attention. La relation d'égalité se note ==, non =. Voici une faute possible chez les nouveaux venus à C en provenance de Pascal qui, pour traduire le bout de Pascal if a = 0 then etc., écrivent

Sélectionnez

if (a = 0) etc.

Du point de vue syntaxique cette construction est correcte, mais elle est loin d'avoir l'effet escompté. En tant qu'expression, a = 0 vaut 0 (avec, comme effet de bord, la mise à zéro de a). Bien sûr, il fallait écrire

Sélectionnez

if (a == 0) etc.

II-B-16. Opérateurs de bits & | ^

Ces opérateurs désignent les opérations logiques et, ou et ou exclusif sur les bits des représentations internes des valeurs des opérandes. Format :

Image non disponible

exp1 et exp2 doivent être d'un type entier. Cette expression représente un objet de type entier dont le codage interne est construit bit par bit à partir des bits correspondants des valeurs de exp1 et exp2, selon la table suivante, dans laquelle (exp)i signifie « le ieme bit de exp » :

Image non disponible

Avant l'évaluation du résultat, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces ex- pressions ne sont pas des lvalue.

Exemple. Il n'y a pas beaucoup d'exemples « de haut niveau » 19 d'utilisation de ces opérateurs. Le plus utile est sans doute la réalisation d'ensembles de nombres naturels inférieurs à une certaine constante pas trop grande, c'est-à-dire des sous-ensembles de l'intervalle d'entiers [ 0 ... N-1 ], N valant 8, 16 ou 32.

Par exemple, les bibliothèques graphiques comportent souvent une fonction qui effectue l'affichage d'un texte affecté d'un ensemble d'attributs (gras, italique, souligné, capitalisé, etc.). Cela ressemble à ceci :

Sélectionnez

void afficher(char *texte, unsigned long attributs);

Pour faire en sorte que l'argument attributs puisse représenter un ensemble d'attributs quelconque, on associe les attributs à des entiers conventionnels :

Sélectionnez

#define GRAS 1 			/* en binaire: 00...000001 */
#define ITALIQUE 2 		/* en binaire: 00...000010 */
#define SOULIGNE 4 		/* en binaire: 00...000100 */
#define CAPITALISE 8 		/* en binaire: 00...001000 */
/* etc. */

Les valeurs utilisées pour représenter les attributs sont des puissances de 2 distinctes, c'est-à-dire des nombres qui, écrits en binaire, comportent un seul 1 placé différemment de l'un à l'autre. L'utilisateur d'une telle fonction emploie l'opérateur | pour composer, lors de l'appel, l'ensemble d'attributs qu'il souhaite :

Sélectionnez

afficher("Bonjour", GRAS | SOULIGNE); /* affichage en gras et italique */

(avec les constantes de notre exemple, la valeur de l'expression GRAS | SOULIGNE est un nombre qui, en binaire, s'écrit 00...000101).

De son coté, le concepteur de la fonction utilise l'opérateur & pour savoir si un attribut appartient ou non à l'ensemble donné :

Sélectionnez

    ...
    if ((attributs & GRAS) != 0)
        prendre les dispositions nécessaires pour afficher en gras
    else if ((attributs & ITALIQUE) != 0)
        prendre les dispositions nécessaires pour afficher en italique
    ...

19 En effet, ces opérateurs sont principalement destinés à l'écriture de logiciel de bas niveau, comme les pilotes de périphérique (les célèbres « drivers »), c'est-à-dire des programmes qui reçoivent les informations transmises par les composants matériels ou qui commandent le fonctionnement de ces derniers. Dans de telles applications on est souvent aux pries avec des nombres dont chaque bit a une signification propre, indépendante des autres. Ces programmes sortent du cadre de ce cours.

II-B-17. Connecteurs logiques && et ||

Opérations : conjonction et disjonction. Format :

Image non disponible

exp1 et exp2 sont deux expressions de n'importe quel type. Cette expression représente un élément de type int parmi { 0, 1 } défini de la manière suivante :

Pour évaluer exp1 && exp2 : exp1 est évaluée d'abord et

  • si la valeur de exp1 est nulle, exp2 n'est pas évaluée et exp1 && exp2 vaut 0 ;
  • sinon exp2 est évaluée et exp1 && exp2 vaut 0 ou 1 selon que la valeur de exp2 est nulle ou non.

Pour évaluer exp1 || exp2 : exp1 est évaluée d'abord et

  • si la valeur de exp1 est non nulle, exp2 n'est pas évaluée et exp1 || exp2 vaut 1 ;
  • sinon exp2 est évaluée et exp1 || exp2 vaut 0 ou 1 selon que la valeur de exp2 est nulle ou non.

Ces expressions ne sont pas des lvalue.

Applications. Ainsi, C garantit que le premier opérande sera évalué d'abord et que, s'il suffit à déterminer le résultat de la conjonction ou de la disjonction, alors le second opérande ne sera même pas évalué. Pour le programmeur, cela est une bonne nouvelle. En effet, il n'est pas rare qu'on écrive des conjonctions dont le premier opérande « protège » (dans l'esprit du programmeur) le second ; si cette protection ne fait pas partie de la sémantique de l'opérateur pour le langage utilisé, le programme résultant peut être faux. Considérons l'exemple suivant : un certain tableau table est formé de nombre chaines de caractères ; soit ch une variable chaine. L'opération « recherche de ch dans table » peut s'écrire :

Sélectionnez

    ...
    i = 0;
    while (i < nombre && strcmp(table[i], ch) != 0)
        i++;
        ...

Ce programme est juste parce que la condition strcmp(table[i], ch) != 0 n'est évaluée qu'après avoir vérifié que la condition i < nombre est vraie (un appel de strcmp(table[i], ch) avec un premier argument table[i] invalide peut avoir des conséquences tout à fait désastreuses).

Une autre conséquence de cette manière de concevoir && et || est stylistique. En C la conjonction et la disjonction s'évaluent dans l'esprit des instructions plus que dans celui des opérations. L'application pratique de cela est la possibilité d'écrire sous une forme fonctionnelle des algorithmes qui dans d'autres langages seraient séquentiels. Voici un exemple : le prédicat qui caractérise la présence d'un élément dans une liste chainée. Version (récursive) habituelle :

Sélectionnez

int present(INFO x, LISTE L) { /* l'information x est-elle dans la liste L ? */
    if (L == NULL)
        return 0;
    else if (L->info == x)
        return 1;
    else
        return present(x, L->suivant);
}

Version dans un style fonctionnel, permise par la sémantique des opérateurs && et || :

Sélectionnez

int existe(INFO x, LISTE L) { /* l'information x est-elle dans la liste L ? */
    return L != NULL && (x == L->info || existe(x, L->suivant));
}

II-B-18. Expression conditionnelle ? :

Opération : sorte de if...then...else... présenté sous forme d'expression, c'est-à-dire renvoyant une valeur.

Format :

Sélectionnez

    exp0 ? exp1 : exp2

exp0 est d'un type quelconque. exp1 et exp2 doivent être de types compatibles. Cette expression est évaluée de la manière suivante :

La condition exp0 est évaluée d'abord

  • si sa valeur est non nulle, exp1 est évaluée et définit la valeur de l'expression conditionnelle. Dans ce cas, exp2 n'est pas évaluée ;
  • sinon, exp2 est évaluée et définit la valeur de l'expression conditionnelle. Dans ce cas, exp1 n'est pas évaluée.

L'expression exp0 ?exp1:exp2 n'est pas une lvalue.

Exemple. L'opérateur conditionnel n'est pas forcément plus facile à lire que l'instruction conditionnelle, mais permet quelquefois de réels allégements du code. Imaginons un programme devant afficher un des textes non ou oui selon que la valeur d'une variable reponse est nulle ou non. Solutions classiques de ce micro-problème :

Sélectionnez

    if (reponse)
        printf("la réponse est oui");
    else
        printf("la réponse est non");

ou bien, avec une variable auxiliaire :

Sélectionnez

    char *texte;
    ...
    if (reponse)
        texte = "oui";
    else
        texte = "non";
    printf("la réponse est %s", texte);

Avec l'opérateur conditionnel c'est bien plus compact :

Sélectionnez

    printf("la réponse est %s", reponse ? "oui" : "non");

II-B-19. Affectation =

Opération : affectation, considérée comme une expression. Format :

Sélectionnez

        exp1 = exp2

exp1 doit être une lvalue. Soit type1 le type de exp1 ; l'affectation ci-dessus représente le même objet que

Sélectionnez

        ( type1 ) exp2

(la valeur de exp2 convertie dans le type de exp1), avec pour effet de bord le rangement de cette valeur dans l'emplacement de la mémoire déterminé par exp1.

L'expression exp1 = exp2 n'est pas une lvalue.

Contrairement à ce qui se passe dans d'autres langages, une affectation est donc considérée en C comme une expression : elle « fait » quelque chose, mais aussi elle « vaut » une certaine valeur et, à ce titre, elle peut figurer comme opérande dans une sur-expression. On en déduit la possibilité des affectations multiples, comme dans l'expression :

Sélectionnez

    a = b = c = 0;

comprise comme a = (b = (c = 0)). Elle aura donc le même effet que les trois affectations a = 0 ; b = 0 ;

c = 0 ; Autre exemple, lecture et traitement d'une suite de caractères dont la fin est indiquée par un point :

Sélectionnez

    ...
    while ((c = getchar()) != '.')
    exploitation de c
    ...

Des contraintes pèsent sur les types des deux opérandes d'une affectation exp1 = exp2. Lorsqu'elles sont satisfaites on dit que exp1 et exp2 sont compatibles pour l'affectation. Essentiellement :

  • deux types numériques sont toujours compatibles pour l'affectation. La valeur de exp2 subit éventuellement une conversion avant d'être rangée dans exp1. La manière de faire cette conversion est la même que dans le cas de l'opérateur de conversion (cf. section II.B.12) ;
  • si exp1 et exp2 sont de types adresses distincts, certains compilateurs (dont ceux conformes à la norme ANSI) les considéreront comme incompatibles tandis que d'autres compilateurs se limiteront à donner un message d'avertissement lors de l'affectation de exp2 à exp1 ;
  • dans les autres cas, exp1 et exp2 sont compatibles pour l'affectation si et seulement si elles sont de même type.

D'autre part, de la signification du nom d'une variable de type tableau (cf. V.A.1) et de celle d'une variable de type structure (cf. V.B.1) on déduit que :

  • on ne peut affecter un tableau à un autre, même s'ils sont définis de manière rigoureusement identique (un nom de tableau n'est pas une lvalue) ;
  • on peut affecter le contenu d'une variable de type structure ou union à une autre, à la condition qu'elles aient été explicitement déclarées comme ayant exactement le même type.

II-B-20. Autres opérateurs d'affectation += *= etc.

Opération binaire vue comme une modification du premier opérande. Format :

Image non disponible

exp1 doit être une lvalue. Cela fonctionne de la manière suivante : si • représente l'un des opérateurs + - * / % >> << & ^ |, alors

Sélectionnez

    exp1 • = exp2

peut être vue comme ayant la même valeur et le même effet que

Sélectionnez

    exp1 = exp1 • exp2

mais avec une seule évaluation de exp1. L'expression résultante n'est pas une lvalue.

Exemple. écrivons la version itérative usuelle de la fonction qui calcule xn avec x flottant et n entier non négatif :

Sélectionnez

double puissance(double x, int n) {
    double p = 1;
    while (n != 0) {
        if (n % 2 != 0) /* n est-il impair ? */
            p *= x;
        x *= x;
        n /= 2;
    }
    return p;
}

Remarque. En examinant ce programme, on peut faire les mêmes commentaires qu'à l'occasion de plusieurs autres éléments du langage C :

  • l'emploi de ces opérateurs constitue une certaine optimisation du programme. En langage machine, la suite d'instructions qui traduit textuellement a += c est plus courte que celle qui correspond à a = b + c. Or, un compilateur rustique traitera a = a + c comme un cas particulier de a = b + c, sans voir l'équivalence avec la première forme.
  • hélas, l'emploi de ces opérateurs rend les programmes plus denses et moins faciles à lire, ce qui favorise l'apparition d'erreurs de programmation.
  • de nos jours les compilateurs de C sont devenus assez perfectionnés pour déceler automatiquement la possibilité de telles optimisations. Par conséquent, l'argument de l'efficacité ne justifie plus qu'on obscurcisse un programme par l'emploi de tels opérateurs.

Il faut savoir cependant qu'il existe des situations ou l'emploi de ces opérateurs n'est pas qu'une question d'efficacité. En effet, si exp1 est une expression sans effet de bord, alors les expressions exp1 • = exp2 et exp1 = exp1 • exp2 sont réellement équivalentes. Mais ce n'est plus le cas si exp1 a un effet de bord. Il est clair, par exemple, que les deux expressions suivantes ne sont pas équivalentes (la première est tout simplement erronée) 20 :

Sélectionnez

nombre[rand() % 100] = nombre[rand() % 100] + 1; 	/* ERRONE ! */

nombre[rand() % 100] += 1; 							/* CORRECT */

II-B-21. L'opérateur virgule ,

Opération : évaluation en séquence. Format :

Sélectionnez

    exp1 , exp2

exp1 et exp2 sont quelconques. L'évaluation de exp1 , exp2 consiste en l'évaluation de exp1 suivie de l'évaluation de exp2. L'expression exp1 , exp2 possède le type et la valeur de exp2 ; le résultat de l'évaluation de exp1 est « oublié », mais non son éventuel effet de bord (cet opérateur n'est utile que si exp1 a un effet de bord). L'expression exp1 , exp2 n'est pas une lvalue.

Exemple. Un cas fréquent d'utilisation de cet opérateur concerne la boucle for (cf. section III.B.6), dont la syntaxe requiert exactement trois expressions à trois endroits bien précis. Parfois, certaines de ces expressions doivent être doublées :

Sélectionnez

...
for (pr = NULL, p = liste; p != NULL; pr = p, p = p->suivant)
    if (p->valeur == x)
        break;
...

Remarque syntaxique. Dans des contextes ou des virgules apparaissent normalement, par exemple lors d'un appel de fonction, des parenthèses sont requises afin de forcer le compilateur à reconnaitre l'opérateur virgule. Par exemple, l'expression

Sélectionnez

    uneFonction(exp1, (exp2, exp3), exp4);

représente un appel de uneFonction avec trois arguments effectifs : les valeurs de exp1, exp3 et exp4. Au passage, l'expression exp2 aura été évaluée. Il est certain que exp2 aura été évaluée avant exp3, mais les spécifications du langage ne permettent pas de placer les évaluations de exp1 et exp4 par rapport à celles de exp2 et exp3.

20 La fonction rand() renvoie un entier aléatoire distinct chaque fois qu'elle est appelée.

II-C. Autres remarques

II-C-1. Les conversions usuelles

Les règles suivantes s'appliquent aux expressions construites à l'aide d'un des opérateurs *, /, %, +, -, <, <=, >, >=, ==, !=, &, ^, |, && et ||. Mutatis mutandis, elles s'appliquent aussi à celles construites avec les opérateurs *=, /=, %=, +=, -=, <<=, >>=, &=, ^= et |=.

Dans ces expressions, les opérandes subissent certaines conversions avant que l'expression ne soit évaluée. En C ANSI ces conversions, dans l'ordre « logique » ou elles sont faites, sont les suivantes :

  • Si un des opérandes est de type long double, convertir l'autre dans le type long double ; le type de l'expression sera long double.
  • Sinon, si un des opérandes est de type double, convertir l'autre dans le type double ; le type de l'expression sera double.
  • Sinon, si un des opérandes est de type float, convertir l'autre dans le type float ; le type de l'expression sera float 21.
  • Effectuer la promotion entière : convertir les char, les short, les énumérations et les champs de bits en des int. Si l'une des valeurs ne peut pas être représentée dans le type int, les convertir toutes dans le type unsigned int.
  • Ensuite, si un des opérandes est unsigned long, convertir l'autre en unsigned long ; le type de l'expression sera unsigned long.
  • Sinon, si un des opérandes est long et l'autre unsigned int :
    • si un long peut représenter toutes les valeurs unsigned int, alors convertir l'opérande de type unsigned int en long. Le type de l'expression sera long ;
    • sinon, convertir les deux opérandes en unsigned long. Le type de l'expression sera unsigned long.
  • Sinon, si un des opérandes est de type long, convertir l'autre en long ; le type de l'expression sera long.
  • Sinon, si un des opérandes est de type unsigned int, convertir l'autre en unsigned int ; le type de l'ex- pression sera unsigned int.
  • Sinon, et si l'expression est correcte, c'est que les deux opérandes sont de type int ; le type de l'expression sera int.

21 Cette règle est apparue avec le C ANSI : le compilateur accepte de faire des calculs sur des °ottants en simple précision. Dans le C original, elle s'énonce plus simplement : « sinon, si l'un des opérandes est de type float, convertir les deux opérandes dans le type double ; le type de l'expression sera double ».

22 Cette règle compliquée est apparue avec le C ANSI. En C original, le type unsigned « tire vers lui » les autres types.

II-C-2. L'ordre d'évaluation des expressions

Les seules expressions pour lesquelles l'ordre (chronologique) d'évaluation des opérandes est spécifié sont les suivantes :

  • « exp1 && exp2 » et « exp1 || exp2 » : exp1 est évaluée d'abord ; exp2 n'est évaluée que si la valeur de exp1 ne permet pas de conclure ;
  • « exp0 ? exp1 : exp2 » exp0 est évaluée d'abord. Une seule des expressions exp1 ou exp2 est évaluée ensuite ;
  • « exp0 , exp0 » : exp1 est évaluée d'abord, exp2 est évaluée ensuite.

Dans tous les autres cas, C ne garantit pas l'ordre chronologique dans lequel les opérandes intervenant dans une expression ou les arguments effectifs d'un appel de fonction sont évalués ; il ne faut donc pas faire d'hypothèse à ce sujet. La question ne se pose que lorsque ces opérandes et arguments sont à leur tour des expressions complexes ; elle est importante dans la mesure ou C favorise la programmation avec des effets de bord. Par exemple, si i vaut 1, l'expression a[i] + b[i++] peut aussi bien additionner a[1] et b[1] que a[2] et b[1].

L'ordre d'évaluation des opérandes d'une affectation n'est pas fixé non plus. Pour évaluer exp1 = exp2, on peut évaluer d'abord l'adresse de exp1 et ensuite la valeur de exp2, ou bien faire l'inverse. Ainsi, le résultat de l'affectation a[i] = b[i++] est imprévisible.

II-C-3. Les opérations non abstraites

Beaucoup d'opérateurs étudiés dans cette section (opérateurs arithmétiques, comparaisons, logiques, etc.) représentent des opérations abstraites, c'est-à-dire possédant une définition formelle qui ne fait pas intervenir les particularités de l'implantation du langage. Bien sûr, les opérandes sont représentés dans la machine par des configurations de bits, mais seule leur interprétation comme des entités de niveau supérieur (nombres entiers, flottants...) est utile pour définir l'effet de l'opération en question ou les contraintes qu'elle subit.

A l'opposé, un petit nombre d'opérateurs, le complément à un (~), les décalages (<< et >>) et les opérations bit-à-bit (&, ^ et |), n'ont pas forcément de signification abstraite. Les transformations qu'ils effectuent sont définies au niveau des bits constituant le codage des opérandes, non au niveau des nombres que ces opérandes représentent. De telles opérations sont réservées aux programmes qui remplissent des fonctions de très bas niveau, c'est-à-dire qui sont aux points de contact entre la composante logicielle et la composante matérielle d'un système informatique.

L'aspect de cette question qui nous intéresse le plus ici est celui-ci : la portabilité des programmes contenant des opérations non abstraites n'est pas assurée. Ce défaut, qui n'est pas rédhibitoire dans l'écriture de fonctions de bas niveau (ces fonctions ne sont pas destinées à être portées), doit rendre le programmeur très précautionneux dès qu'il s'agit d'utiliser ces opérateurs dans des programmes de niveau supérieur, et le pousser à :

  • isoler les opérations non abstraites dans des fonctions de petite taille bien repérées ;
  • documenter soigneusement ces fonctions ;
  • constituer des jeux de tests validant chacune de ces fonctions.


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • III. Instructions
    • III-A. Syntaxe
    • III-B. Présentation détaillée des instructions
      • III-B-1. Blocs
      • III-B-2. Instruction-expression
      • III-B-3. Etiquettes et instruction goto
      • III-B-4. Instruction if...else...
      • III-B-5. Instructions while et do...while
      • III-B-6. Instruction for
      • III-B-7. Instruction switch
      • III-B-8. Instructions break et continue
      • III-B-9. Instruction return

III. Instructions

III-A. Syntaxe

Dans les descriptions syntaxiques suivantes le suffixe « opt » indique que la formule qu'il qualifie est optionnelle. Une formule avec des points de suspension, comme « élément ... élément », indique un élément pouvant apparaitre un nombre quelconque, éventuellement nul, de fois.

Sélectionnez

instruction
    → instruction-bloc
    → instruction-expression
    → instruction-goto
    → instruction-if
    → instruction-while
    → instruction-do
    → instruction-for
    → instruction-break
    → instruction-continue
    → instruction-switch
    → instruction-return
    → instruction-vide
    → identificateur : instruction
Sélectionnez

instruction-bloc
    → f déclaration ... déclaration
Sélectionnez

instruction-expression
    → expression ;
Sélectionnez

instruction-goto
    → goto identif ;
Sélectionnez

instruction-if
    → if ( expression ) instruction else instruction
    → if ( expression ) instruction
Sélectionnez

instruction-while
    → while ( expression ) instruction
Sélectionnez

instruction-do
    → do instuction while ( expression ) ;
Sélectionnez

instruction-for
    → for ( expressionopt ; expressionopt ; expressionopt ) instruction
Sélectionnez

instruction-break
    → break ;
Sélectionnez

instruction-continue
    → continue ;
Sélectionnez

instruction-switch
    → switch ( expression ) { instruction-ou-case ... instruction-ou-case }
Sélectionnez

instruction-ou-case
    → case expression-constante : instructionopt
    → default : instruction
    → instruction
Sélectionnez

instruction-return
    → return expressionopt ;
Sélectionnez

instruction-vide
    → ;

C et le point-virgule

Comme l'indique la syntaxe de l'instruction-bloc, en C le point-virgule n'est pas un séparateur d'instructions mais un terminateur de certaines instructions. Autrement dit, il appartient à la syntaxe de chaque instruction de préciser si elle doit ou non être terminée par un point-virgule, indépendamment de ce par quoi l'instruction est suivie dans le programme.

L'oubli du point-virgule à la fin d'une instruction qui en requiert un est toujours une erreur, quelle que soit la situation de cette instruction. Un surnombre de points-virgules crée des instructions vides.

III-B. Présentation détaillée des instructions

III-B-1. Blocs

Un bloc est une suite de déclarations et d'instructions encadrée par les deux accolades { et }. Du point de vue de la syntaxe il se comporte comme une instruction unique et peut figurer en tout endroit ou une instruction simple est permise.

Le bloc le plus extérieur d'une fonction et les autres blocs plus profonds ont le même statut. En particulier, quelle que soit sa position dans le programme, un bloc peut comporter ses propres déclarations de variables. Sauf si elle est déclarée extern, une variable définie dans un bloc est locale à ce bloc et donc inconnue à l'extérieur. Dans le bloc ou elle est définie, une telle variable masque, sans le détruire, tout autre objet de même nom connu à l'extérieur du bloc.

Sauf si elles sont qualifiées static (ou extern), de telles variables sont créées et éventuellement initialisées lors de l'activation du bloc ; elles sont détruites dès que le contrôle quitte le bloc. Il ne faut donc pas espérer qu'une telle variable conserve sa valeur entre deux passages dans le bloc. Les variables locales aux blocs permettent d'optimiser la gestion de l'espace local. Par exemple, dans un programme tel que

Sélectionnez

if (...) {
    type1 n;
    ...
}
else {
    type2 x;
    ...
}

les variables n et x n'existeront jamais simultanément ; le compilateur peut donc leur allouer le même emplacement de la mémoire.

III-B-2. Instruction-expression

Format :

Sélectionnez

    expression ;

Mais oui, il suffit d'écrire un point-virgule derrière n'importe quelle expression pour en faire une instruction.

Exemples :

Sélectionnez

123; 				a
i++; 				b
x = 2 * x + 3; 		c
printf("%d\n", n); 	d

Intuitivement, une instruction-expression représente l'ordre « évaluez cette expression, ensuite oubliez le résultat ». Il est clair que si l'expression n'a pas d'effet de bord, cela n'aura servi à rien (exemple a). L'aspect utile de cette notion est : toute expression avec effet de bord pourra être évaluée uniquement pour son effet de bord (exemple b).

Avec deux cas particuliers très intéressants :
  • puisque l'affectation est une expression, on retrouve bien l'instruction d'affectation, fondamentale dans tous les langages de programmation (exemple c) ;
  • toute fonction peut être appelée « comme une procédure » (exemple 23 d), c'est-à-dire en ignorant la valeur qu'elle rend.

Remarque. Une conséquence regrettable de tout cela est un piège assez vicieux tendu aux pascaliens.
Imaginons que lirecaractere soit le nom d'une fonction sans argument. L'appel correct de cette fonction s'écrit :

Sélectionnez

    lirecaractere();

Cependant, puisque le nom d'une fonction est une expression (une constante valant l'adresse de la fonction) et que toute expression suivie d'un point-virgule est une instruction correcte, l'énoncé

Sélectionnez

    lirecaractere;

sera trouvé légal par le compilateur. Or cette expression (tout à fait analogue à l'exemple a ci-dessus) ne produit pas l'appel de la fonction et ne traduit donc probablement pas la pensée du programmeur.

23On verra le moment venu (cf. section VII.B.5) que printf rend un résultat parfois utile.

III-B-3. Etiquettes et instruction goto

Format :

Sélectionnez

    étiquette : instruction

Une étiquette est un identificateur ; elle doit être placée devant une fonction, séparée de celle-ci par un caractère deux points. Elle n'a pas à faire l'objet d'une déclaration explicite ; il suffit de l'écrire devant une instruction pour qu'elle soit automatiquement connue comme un nom à portée locale. Elle est alors utilisable partout dans la fonction ou elle apparait (avant et après l'instruction qu'elle préfixe) et elle reste inconnue en dehors de la fonction.

L'instruction

Sélectionnez

goto étiquette ;

transfère le contrôle à l'instruction préfixée par l'étiquette en question.

Théoriquement, tout algorithme peut être programmé sans utiliser l'instruction goto. Dans certains langages comme Pascal, elle est utilisée pour obtenir l'abandon d'une structure de contrôle (exemple : une boucle) depuis l'intérieur de la structure. Un tel emploi de goto est avantageusement remplacé en C par l'utilisation des instructions return, break et continue.

Il est donc rare que l'on ait besoin de l'instruction goto en C. Elle ne se révèle utile que lorsqu'il faut abandonner plusieurs structures de contrôle (if, while, for...) imbriquées les unes dans les autres. Exemple :

Sélectionnez

for (i = 0; i < N1; i++) {
    for (j = 0; j <= N2; j++)
        for (k = 0; k <= N2; k++) {
        ...
    if (...)
        goto grande_boucle;
    ...
}
...
grande_boucle: 	/* ici on a quitté les deux boucles internes (sur j et k) */
... 			/* mais on est toujours dans la boucle la plus externe (sur i) */
}

III-B-4. Instruction if...else...

Formats :

Sélectionnez

if (expression)
    instruction1
else
    instruction2

et

Sélectionnez

if (expression)
    instruction1

Dans la première forme, expression est évaluée : si elle est vraie (i.e. non nulle) instruction1 est exécutée ; si elle est fausse (nulle) instruction2 est exécutée. Dans la deuxième forme, expression est évaluée : si elle est vraie instruction1 est exécutée ; sinon, rien n'est exécuté.

On notera que l'expression conditionnelle doit figurer entre parenthèses. Celles-ci font partie de la syntaxe du if, non de celle de l'expression.

Lorsque plusieurs instructions if sont imbriquées, il est convenu que chaque else se rapporte au dernier if pour lequel le compilateur a rencontré une condition suivie d'exactement une instruction. Le listing du programme peut (et doit !) traduire cela par une indentation (marge blanche) expressive, mais il ne faut pas oublier que le compilateur ne tient pas compte des marges. Par exemple, le programme suivant est probablement incorrect ; en tout cas, son indentation ne traduit pas convenablement les rapports entre les if et les else :

Sélectionnez

if (nombrePersonnes != 0)
    if (nombrePersonnes != nombreAdultes)
        printf("Il y a des enfants!");
else
    printf("Il n'y a personne!");

Ce problème se pose dans tous les langages qui offrent deux variétés d'instruction conditionnelle. On le résout soit par l'utilisation d'instructions vides :

Sélectionnez

if (nombrePersonnes != 0)
if (nombrePersonnes != nombreAdultes)
printf("Il y a des enfants!");
else
;
else
printf("Il n'y a personne!");

soit, plus simplement, en utilisant des blocs :

Sélectionnez

if (nombrePersonnes != 0) {
    if (nombrePersonnes != nombreAdultes)
        printf("Il y a des enfants!");
}
else
    printf("Il n'y a personne!");

Remarque. La syntaxe prévoit exactement une instruction entre la condition et le else. Par conséquent, un excès de points-virgules à la suite de instruction1 constitue une erreur. Voici une faute qu'on peut faire quand on débute :

Sélectionnez

if (nombrePersonnes != 0) {
    if (nombrePersonnes != nombreAdultes)
        printf("Il y a des enfants!");
};
else
    printf("Il n'y a personne!");

Il y a maintenant deux instructions entre la ligne du if et celle du else : une instruction-bloc f ... g et une instruction vide « ; ». Le compilateur signalera donc une erreur sur le else.

III-B-5. Instructions while et do...while

Ces instructions correspondent respectivement aux instructions while...do... et repeat...until... du langage Pascal. Notez que la syntaxe exige que la condition figure entre parenthèses. Formats :

Sélectionnez

while (expression)
    instruction

et

Sélectionnez

do
    instruction
while (expression);

Le fonctionnement de ces instructions est décrit par les organigrammes de la figure 4. Fondamentalement, il s'agit de réitérer l'exécution d'une certaine instruction tant qu'une certaine instruction, vue comme une condition, reste vraie. Dans la structure while on vérifie la condition avant d'exécuter l'instruction, tandis que dans la structure do...while on la vérifie après.

Fig. 4 - Instruction while (à gauche) et do...while (à droite)
Fig. 4 - Instruction while (à gauche) et do...while (à droite)

L'instruction do...while... exécute donc au moins une fois l'instruction qui constitue son corps avant d'évaluer la condition de continuation. C'est en cela qu'elle est l'analogue de l'instruction repeat...until du Pascal. Mais on notera que la condition figurant dans une instruction do...while (« faire tant que... ») et celle qui figurerait dans une instruction repeat...until équivalente (« répéter jusqu'à ce que... ») sont inverses l'une de l'autre.

III-B-6. Instruction for

Format :

Sélectionnez

for ( expr1 ; expr2 ; expr3 )
    instruction

Par définition, cette construction équivaut strictement à celle-ci :

Sélectionnez

expr1 ;
while (expr2) {
instruction
expr3;
}

Ainsi, dans la construction for(expr1 ; expr2 ; expr3) :

  • expr1 est l'expression qui effectue les initialisations nécessaires avant l'entrée dans la boucle ;
  • expr2 est le test de continuation de la boucle ; il est évalué avant l'exécution du corps de la boucle ;
  • expr3 est une expression (par exemple une incrémentation) évaluée à la fin du corps de la boucle.

Par exemple, l'instruction Pascal :

Sélectionnez

for i:=0 to 9 do
    t[i]:=0

se traduit tout naturellement en C

Sélectionnez

for (i = 0; i < 10; i++)
t[i] = 0;

Les expressions expr1 et expr3 peuvent être absentes (les points-virgules doivent cependant apparaitre). Par exemple

Sélectionnez

for ( ; expr2 ; )
    instruction

équivaut à

while (expr2)
    instruction

La condition de continuation expr2 peut elle aussi être absente. On considère alors qu'elle est toujours vraie. Ainsi, la boucle « indéfinie » peut se programmer :

Sélectionnez

for ( ; ; )
    instruction

Bien entendu, il faut dans ce cas que l'abandon de la boucle soit programmé à l'intérieur de son corps (sinon cette boucle indéfinie devient infinie !). Exemple :

Sélectionnez

for (;;) {
    printf("donne un nombre (0 pour sortir): ");
    scanf("%d", &n);
    if (n == 0)
break;
...
exploitation de la donnée n
...
}

Lorsque l'abandon de la boucle correspond aussi à l'abandon de la procédure courante, break est avantageusement remplacée par return comme dans :

Sélectionnez

char *adresse_de_fin(char *ch) {
    /* renvoie adresse du caractère qui suit la chaine ch */
for (;;)
    if (*ch++ == 0)
        return ch;
}

III-B-7. Instruction switch

Format :

Sélectionnez

switch ( expression )
    corps

Le corps de l'instruction switch prend la forme d'un bloc f...g renfermant une suite d'instructions entre lesquelles se trouvent des constructions de la forme

Sélectionnez

case expression-constante :

ou bien

Sélectionnez

default :

Le fonctionnement de cette instruction est le suivant : expression est évaluée :

  • s'il existe un énoncé case avec une constante qui égale la valeur de expression, le contrôle est transféré à l'instruction qui suit cet énoncé ;
  • si un tel case n'existe pas, et si l'énoncé default existe, alors le contrôle est transféré à l'instruction qui suit l'énoncé default ;
  • si la valeur de expression ne correspond à aucun case et s'il n'y a pas d'énoncé default, alors aucune instruction n'est exécutée.

Attention. Lorsqu'il y a branchement réussi à un énoncé case, toutes les instructions qui le suivent sont exécutées, jusqu'à la fin du bloc ou jusqu'à une instruction de rupture (break). Autrement dit, l'instruction switch s'apparente beaucoup plus à une sorte de goto « paramétré » (par la valeur de l'expression) qu'à l'instruction case...of... de Pascal.

Exemple (idiot) :

Sélectionnez

j = 0;
switch (i) {
    case 3:
        j++;
    case 2:
        j++;
    case 1:
        j++;
}

Si on suppose que i ne peut prendre que les valeurs 0 ... 3, alors l'instruction ci-dessus a le même effet que l'affectation j = i.

Pour obtenir un comportement similaire à celui du case...of... de Pascal, on doit utiliser l'instruction break, comme dans l'exemple suivant, dans lequel on compte la fréquence de chaque chiffre et des caractères blancs dans un texte :

Sélectionnez

nb_blancs = nb_autres = 0;
for (i = 0; i < 10; )
nb_chiffre[i++] = 0;
while ((c = getchar()) != EOF)
    switch (c) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            nb_chiffre[c - '0']++;
            break;
        case ' ':
        case '\n':
        case '\t':
            nb_blancs++;
            break;
            default:
            nb_autres++;
}

III-B-8. Instructions break et continue

Formats :

Sélectionnez

    break;
    continue;

Dans la portée d'une structure de contrôle (instructions for, while, do et switch), l'instruction break provoque l'abandon de la structure et le passage à l'instruction écrite immédiatement derrière. L'utilisation de break ailleurs qu'à l'intérieur d'une de ces quatre instructions constitue une erreur (que le compilateur signale).

Par exemple la construction

Sélectionnez

for (expr1 ; expr2 ; expr3) {
    ...
    break;
    ...
}

équivaut à

Sélectionnez

{
for (expr1 ; expr2 ; expr3) {
...
goto sortie;
...
}
sortie: ;
}

L'instruction continue est moins souvent utilisée. Dans la portée d'une structure de contrôle de type boucle (while, do ou for), elle produit l'abandon de l'itération courante et, le cas échéant, le démarrage de l'itération suivante. Elle agit comme si les instructions qui se trouvent entre l'instruction continue et la fin du corps de la boucle avaient été supprimées pour l'itération en cours : l'exécution continue par le test de la boucle (précédé, dans le cas de for, par l'exécution de l'expression d'incrémentation).

Lorsque plusieurs boucles sont imbriquées, c'est la plus profonde qui est concernée par les instructions break et continue. Dans ce cas, l'emploi de ces instructions ne nous semble pas œuvrer pour la clarté des programmes.

III-B-9. Instruction return

Formats :

Sélectionnez

return expression ;

et

Sélectionnez

return ;

Dans un cas comme dans l'autre l'instruction return provoque l'abandon de la fonction en cours et le retour à la fonction appelante.

Dans la première forme expression est évaluée ; son résultat est la valeur que la fonction renvoie à la fonction appelante ; c'est donc la valeur que l'appel de la fonction représente dans l'expression ou il figure. Si nécessaire, la valeur de expression est convertie dans le type de la fonction (déclaré dans l'en-tête), les conversions autorisées étant les mêmes que celles faites à l'occasion d'une affectation.

Dans la deuxième forme, la valeur retournée par la fonction reste indéterminée. On suppose dans ce cas que la fonction appelante n'utilise pas cette valeur ; il est donc prudent de déclarer void cette fonction. Absence d'instruction return dans une fonction. Lorsque la dernière instruction (dans l'ordre de l'exécution) d'une fonction est terminée, le contrôle est rendu également à la procédure appelante. Tout se passe comme si l'accolade fermante qui termine le corps de la fonction était en réalité écrite sous la forme

... return;


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • IV. Fonctions
    • IV-A. Syntaxe ANSI ou "avec prototype"
      • IV-A-1. Définition
      • IV-A-2. Type de la fonction et des arguments
      • IV-A-3. Appel des fonctions
      • IV-A-4. Déclaration "externe" d'une fonction
    • IV-B. Syntaxe originale ou "sans prototype"
      • IV-B-1. Déclaration et définition
      • IV-B-2. Appel
      • IV-B-3. Coexistence des deux syntaxes
    • IV-C. Arguments des fonctions
      • IV-C-1. Passage des arguments
      • IV-C-2. Arguments de type tableau
      • IV-C-3. Arguments par adresse
      • IV-C-4. Arguments en nombre variable

IV. Fonctions

Beaucoup de langages distinguent deux sortes de sous-programmes 24 : les fonctions et les procédures. L'appel d'une fonction est une expression, tandis que l'appel d'une procédure est une instruction. Ou, si on préfère, l'appel d'une fonction renvoie un résultat, alors que l'appel d'une procédure ne renvoie rien.

En C on retrouve ces deux manières d'appeler les sous-programmes, mais du point de la syntaxe le langage ne connait que les fonctions. Autrement dit, un sous-programme est toujours supposé renvoyer une valeur, même lorsque celle-ci n'a pas de sens ou n'a pas été spécifiée. C'est pourquoi on ne parlera ici que de fonctions. C'est dans la syntaxe et la sémantique de la définition et de l'appel des fonctions que résident les principales différences entre le C original et le C ANSI. Nous expliquons principalement le C ANSI, rassemblant dans une section spécifique (cf. section IV.B) la manière de faire du C original.

24La notion de sous-programme (comme les procedures de Pascal, les subroutines de Fortran, etc.) est supposée ici connue du lecteur.

IV-A. Syntaxe ANSI ou "avec prototype"

IV-A-1. Définition

Une fonction se définit par la construction :

Sélectionnez

typeopt ident ( déclaration-un-ident , ... déclaration-un-ident )
    instruction-bloc

Notez qu'il n'y a pas de point-virgule derrière la ")" de la première ligne. La présence d'un point-virgule à cet endroit provoquerait des erreurs bien bizarres, car la définition serait prise pour une déclaration (cf. section IV.A.4).

La syntaxe indiquée ici est incomplète ; elle ne convient qu'aux fonctions dont le type est défini simplement, par un identificateur. On verra ultérieurement (cf. section V.D.1) la manière de déclarer des fonctions rendant un résultat d'un type plus complexe.

La première ligne de la définition d'une fonction s'appelle l'en-tête, ou parfois le prototype, de la fonction. Chaque formule déclaration-un-ident possède la même syntaxe qu'une déclaration de variable 25.

Exemple.

Sélectionnez

int extract(char *dest, char *srce, int combien) {
        /* copie dans dest les combien premiers caractères de srce */
        /* renvoie le nombre de caractères effectivement copiés */
    int compteur;
    for (compteur = 0; compteur < combien && *srce != '\0'; compteur++)
        *dest++ = *srce++;
        *dest = '\0';
    return compteur;
}

Contrairement à d'autres langages, en C on ne peut pas définir une fonction à l'intérieur d'une autre : toutes les fonctions sont au même niveau, c'est-à-dire globales. C'est le cas notamment de main, qui est une fonction comme les autres ayant pour seule particularité un nom convenu.

25Restriction : on n'a pas le droit de « mettre en facteur » un type commun à plusieurs arguments. Ainsi, l'en-tête de la fonction extract donnée en exemple ne peut pas s'écrire sous la forme char *extract(char *dest, *srce, int n).

IV-A-2. Type de la fonction et des arguments

L'en-tête de la fonction définit le type des objets qu'elle renvoie. Ce peut être :

  • tout type numérique ;
  • tout type pointeur ;
  • tout type struct ou union.

Si le type de la fonction n'est pas indiqué, le compilateur suppose qu'il s'agit d'une fonction à résultat entier. Ainsi, l'exemple précédent aurait aussi pu être écrit de manière équivalente (mais cette pratique n'est pas conseillée) :

Sélectionnez

extract(char *dest, char *srce, int combien)
/* etc. */

Fonctions sans résultat. Lorsqu'une fonction ne renvoie pas une valeur, c'est-à-dire lorsqu'elle correspond plus à une procédure qu'à une vraie fonction, il est prudent de la déclarer comme rendant un objet de type voidvoid. Ce type est garanti incompatible avec tous les autres types : une tentative d'utilisation du résultat de la fonction provoquera donc une erreur à la compilation. Le programmeur se trouve ainsi à l'abri d'une utilisation intempestive du résultat de la fonction. Exemple :

Sélectionnez

void extract(char *dest, char *srce, int combien) {
        /* copie dans dest les combien premiers caractères de srce */
        /* maintenant cette fonction ne renvoie rien */
    int compteur;
    for (compteur = 0; compteur < combien && *srce != '\0'; compteur++)
        *dest++ = *srce++;
    *dest = '\0';
}

Fonctions sans arguments. Lorsqu'une fonction n'a pas d'arguments, sa définition prend la forme

Sélectionnez

typeopt ident ( void )
    instruction-bloc

On notera que, sauf cas exceptionnel, on ne doit pas écrire une paire de parenthèses vide dans la déclaration ou la définition d'une fonction. En effet, un en-tête de la forme

Sélectionnez

type ident()

ne signifie pas que la fonction n'a pas d'arguments, mais (cf. section IV.B.2) que le programmeur ne souhaite pas que ses appels soient contrôles par le compilateur.

IV-A-3. Appel des fonctions

L'appel d'une fonction se fait en écrivant son nom, suivi d'une paire de parenthèses contenant éventuellement une liste d'arguments effectifs.

Notez bien que les parenthèses doivent toujours apparaitre, même si la liste d'arguments est vide : si un nom de fonction apparait dans une expression sans les parenthèses, alors il a la valeur d'une constante adresse (l'adresse de la fonction) et aucun appel de la fonction n'est effectué.

Passage des arguments. En C, le passage des arguments se fait par valeur. Cela signifie que les arguments formels 26 de la fonction représentent d'authentiques variables locales initialisées, lors de l'appel de la fonction, par les valeurs des arguments effectifs 27 correspondants.

Supposons qu'une fonction ait été déclarée ainsi

Sélectionnez

type fonction ( type1 arg formel1 , ... typek arg formelk )
    etc.

alors, lors d'un appel tel que

Sélectionnez

fonction ( arg effectif 1 , ... arg effectif k )

la transmission des valeurs des arguments se fait comme si on exécutait les affectations :

Sélectionnez

arg formel1 = arg effectif 1
...
arg formelk = arg effectif k

Cela a des conséquences très importantes :

  • les erreurs dans le nombre des arguments effectifs sont détectées et signalées,
  • si le type d'un argument effectif n'est pas compatible avec celui de l'argument formel correspondant, une erreur est signalée,
  • les valeurs des arguments effectifs subissent les conversions nécessaires avant d'être rangées dans les arguments formels correspondants (exactement les mêmes conversions qui sont faites lors des affectations).

Remarque. Le langage ne spécifie pas l'ordre chronologique des évaluations des arguments effectifs d'un appel de fonction. Ces derniers doivent donc être sans effets de bord les uns sur les autres. Par exemple, il est impossible de prévoir quelles sont les valeurs effectivement passées à une fonction lors de l'appel :

Sélectionnez

x = une_fonction(t[i++], t[i++]); /* ERREUR !!! */

Si i0 est la valeur de i juste avant l'exécution de l'instruction ci-dessus, alors cet appel peut aussi bien se traduire par une fonction(t[i0], t[i0 + 1]) que par une fonction(t[i0 + 1], t[i0]) ou même par une fonction(t[i0], t[i0]). Ce n'est sûrement pas indifférent !

Appel d'une fonction inconnue. En C une fonction peut être appelée alors qu'elle n'a pas été définie (sous-entendu : entre le début du fichier et l'endroit ou l'appel figure).

Le compilateur suppose alors

  • que la fonction renvoie un int,
  • que le nombre et les types des arguments formels de la fonction sont ceux qui correspondent aux arguments effectifs de l'appel (ce qui, en particulier, empêche les contrôles et conversions mentionnés plus haut).

De plus, ces hypothèses sur la fonction constituent pour le compilateur une première déclaration de la fonction. Toute définition ou déclaration ultérieure tant soit peu différente sera qualifiée de « redéclaration illégale » 28.

Lorsque les hypothèses ci-dessus ne sont pas justes, en particulier lorsque la fonction ne renvoie pas un int, il faut :

  • soit écrire la définition de la fonction appelée avant celle de la fonction appelante,
  • soit écrire, avant l'appel de la fonction, une déclaration externe de cette dernière, comme expliqué à la section 4.1.4.

26 Les arguments formels d'une fonction sont les identificateurs qui apparaissent dans la définition de la fonction, déclarés à l'intérieur de la paire de parenthèses.

27 Les arguments effectifs d'un appel de fonction sont les expressions qui apparaissent dans l'expression d'appel, écrites à l'intérieur de la paire de parenthèses caractéristiques de l'appel.

28C'est une erreur surprenante, car le programmeur, oubliant que l'appel de la fonction a entrainé une déclaration implicite, conçoit sa définition ou déclaration ultérieure comme étant la première déclaration de la fonction.

IV-A-4. Déclaration "externe" d'une fonction

Une déclaration externe d'une fonction est une déclaration qui n'est pas en même temps une définition. On « annonce » l'existence de la fonction (définie plus loin, ou dans un autre fichier) tout en précisant le type de son résultat et le nombre et les types de ses arguments, mais on ne donne pas son corps, c'est-à-dire les instructions qui la composent.

Cela se fait en écrivant

  • soit un en-tête identique à celui qui figure dans la définition de la fonction (avec les noms des arguments formels), suivi d'un point-virgule ;
  • soit la formule obtenue à partir de l'en-tête précédent, en y supprimant les noms des arguments formels.

Par exemple, si une fonction a été définie ainsi

Sélectionnez

void truc(char dest[80], char *srce, unsigned long n, float x) {
    corps de la fonction
}

alors des déclarations externes correctes sont :

Sélectionnez

extern void truc(char dest[80], char *srce, unsigned long n, float x);

ou

Sélectionnez

ou extern void machin(char [80], char *, unsigned long, float);

Ces expressions sont appelées des prototypes de la fonction. Le mot extern est facultatif, mais le point- virgule est essentiel. Dans la première forme, les noms des arguments formels sont des identificateurs sans aucune portée.

IV-B. Syntaxe originale ou "sans prototype"

IV-B-1. Déclaration et définition

Définition. En syntaxe originale la définition d'une fonction prend la forme

Sélectionnez

typeopt ident ( ident , ... ident )
déclaration ... déclaration
instruction-bloc

Les parenthèses de l'en-tête ne contiennent ici que la liste des noms des arguments formels. Les déclarations de ces arguments se trouvent immédiatement après l'en-tête, avant l'accolade qui commence le corps de la fonction.

Exemple :

Sélectionnez

int extract(dest, srce, combien)
        /* copie dans dest les combien premiers caractères de srce */
        /* renvoie le nombre de caractères effectivement copiés */
    char *dest, char *srce;
    int combien;
{
    int compteur;
    for (compteur = 0; compteur < combien && *srce != '\0'; compteur++)
    *dest++ = *srce++;
        *dest = '\0';
    return compteur;
}

Déclaration externe. En syntaxe originale, la déclaration externe de la fonction précédente prend une des formes suivantes :

Sélectionnez

extern int extract();

ou

Sélectionnez

int extract();

Comme on le voit, ni les types, ni même les noms, des arguments formels n'apparaissent dans une déclaration externe 29.

29Par conséquent, les déclarations externes montrées ici sont sans aucune utilité, puisque int est le type supposé des fonctions non déclarées.

IV-B-2. Appel

En syntaxe originale, lors d'un appel de la fonction le compilateur ne tient aucun compte ni du nombre ni des types des arguments formels 30, indiqués lors de la définition de la fonction. Chaque argument est évalué séparément, puis

  • les valeurs des arguments effectifs de type char ou short sont converties dans le type int ;
  • les valeurs des arguments effectifs de type float sont converties dans le type double ;
  • les valeurs des arguments effectifs d'autres types sont laissées telles quelles.

Juste avant le transfert du contrôle à la fonction, ces valeurs sont copiées dans les arguments formels correspondants. Ou plus exactement, dans ce que la fonction appelante croit être les emplacements des arguments formels de la fonction appelée : il n'y a aucune vérification de la concordance, ni en nombre ni en type, des arguments formels et des arguments effectifs. Cela est une source d'erreurs très importante. Par exemple, la fonction carre ayant été ainsi définie

Sélectionnez

double carre(x)
    double x;
{
    return x * x;
}

l'appel

Sélectionnez

x = carre(2);

est erroné, car la fonction appelante dépose la valeur entière 2 à l'adresse du premier argument formel (qu'elle « croit » entier). Or la fonction appelée croit recevoir le codage d'un nombre flottant double. Le résultat n'a aucun sens. Des appels corrects auraient été

Sélectionnez

x = carre((double) 2);

ou

Sélectionnez

x = carre(2.0);

30Mais oui, vous avez bien lu ! C'est là la principale caractéristique de la sémantique d'appel des fonctions du C original.

IV-B-3. Coexistence des deux syntaxes

A coté de ce que nous avons appelé la syntaxe ANSI, la syntaxe originale pour la définition et la déclaration externe des fonctions fait partie elle aussi du C ANSI ; de nos jours les programmeurs ont donc le choix entre l'une ou l'autre forme. Lorsque la fonction a été définie ou déclarée sous la syntaxe originale, les appels de fonction sont faits sans vérification ni conversion des arguments effectifs. Au contraire, lorsque la fonction a été spécifiée sous la syntaxe ANSI, ces contrôles et conversions sont effectués.

A cause de cette coexistence, si une fonction n'a pas d'argument, la définition de son en-tête en C ANSI doit s'écrire sous la forme bien peu heureuse :

Sélectionnez

typeopt ident ( void )

car une paire de parenthèses sans rien dedans signifierait non pas que la fonction n'a pas d'arguments, mais qu'elle est déclarée sans prototype, c'est-à-dire que ses appels doivent être traités sans contrôle des arguments.

Exemples :

Sélectionnez

int getchar(void); 	/* une fonction sans arguments dont les appels seront contrôlés */
double moyenne(); 	/* une fonction avec ou sans arguments, aux appels incontrôlés */

IV-C. Arguments des fonctions

IV-C-1. Passage des arguments

L'idée maitresse est qu'en C le passage des arguments des fonctions se fait toujours par valeur. Après avoir fait les conversions opportunes la valeur de chaque argument effectif est affectée à l'argument formel correspondant. Si l'argument effectif est d'un type simple (nombre, pointeur) ou d'un type struct ou union, sa valeur est recopiée dans l'argument formel correspondant, quelle que soit sa taille, c'est-à-dire quel que soit le nombre d'octets qu'il faut recopier.

IV-C-2. Arguments de type tableau

Apparaissant dans la partie exécutable d'un programme, le nom d'un tableau, et plus généralement toute expression déclarée « tableau de T », est considérée comme ayant pour type « adresse d'un T » et pour valeur l'adresse du premier élément du tableau. Cela ne fait pas intervenir la taille effective du tableau 31. Ainsi lorsqu'un argument effectif est un tableau, l'objet effectivement passé à la fonction est uniquement l'adresse du premier élément et il suffit que l'espace réservé dans la fonction pour un tel argument ait la taille, fixe, d'un pointeur.

D'autre part, en C il n'est jamais vérifié que les indices des tableaux appartiennent bien à l'intervalle 0 ... N-1 déterminé par le nombre d'éléments indiqué dans la déclaration. Il en découle la propriété suivante :

Dans la déclaration d'un argument formel t de type « tableau de T » :

  • l'indication du nombre d'éléments de t est sans utilité 32 ;
  • les formules « type t[ ] » et « type *t » sont tout à fait équivalentes ;
  • la fonction pourra être appelée indifféremment avec un tableau ou un pointeur pour argument effectif.

Exemple. L'en-tête « vague » de la fonction suivante

Sélectionnez

int strlen(chaine de caractères s) {
    int i = 0;
    while (s[i] != 0)
        i++;
    return i;
}

peut indifféremment être concrétisée de l'une des trois manières suivantes :

Sélectionnez

int strlen(char s[80])

int strlen(char s[])

int strlen(char *s)

Dans les trois cas, si t est un tableau de char et p l'adresse d'un char, les trois appels suivants sont corrects :

Sélectionnez

l1 = strlen(t);

l2 = strlen(p);

l3 = strlen("Bonjour");

A retenir : parce que les arguments effectifs sont passés par valeur, les tableaux sont passés par adresse. Passage par valeur des tableaux. Cependant, puisque les structures sont passées par valeur, elles fournissent un moyen pour obtenir le passage par valeur d'un tableau, bien que les occasions ou cela est nécessaire semblent assez rares. Il suffit de déclarer le tableau comme le champ unique d'une structure « enveloppe » ne servant qu'à cela. Exemple :

Sélectionnez

struct enveloppe {
    int t[100];
};
void possible_modification(struct enveloppe x) {
    x.t[50] = 2;
}
main() {
    struct enveloppe x;
    x.t[50] = 1;
    possible_modification(x);
    printf("%d\n", x.t[50]);
}

La valeur affichée est 1, ce qui prouve que la fonction appelée n'a modifié qu'une copie locale du tableau enveloppé, c'est-à-dire que celui-ci a bien été passé par valeur (les structures seront vues à la section 5.2.1).

31 Il n'en est pas de même lors de la définition, où la taille du tableau ne peut pas être ignorée (elle est indispensable pour l'allocation de l'espace mémoire).

32 Attention, cette question se complique notablement dans le cas des tableaux multidimensionnels. Cf. 6.2.3

IV-C-3. Arguments par adresse

Les arguments qui sont d'un type simple (char, int, pointeur...) ou bien des struc ou des union sont toujours passés par valeur. Lorsque l'argument effectif est une variable, ce mécanisme empêche qu'elle puisse être modifiée par la fonction appelée. Or une telle modification est parfois souhaitable ; elle requiert que la fonction puisse accéder non seulement à la valeur de la variable, mais aussi à son adresse.

C'est ce qui s'appelle le passage par adresse des arguments. Alors que certains langages le prennent en charge, C ne fournit aucun mécanisme spécifique : le passage de l'adresse de l'argument doit être programmé explicitement en utilisant comme argument un pointeur vers la variable.

Par exemple, supposons avoir à écrire une fonction

Sélectionnez

int quotient(a, b)

censée renvoyer le quotient de la division euclidienne de l'entier a par l'entier b et devant en plus remplacer a par le reste de cette division. On devra la définir :

Sélectionnez

int quotient(int *a, int b) {
    int r = *a / b;
    *a = *a % b;
    return r;
}

Ci-dessus, b représente un entier ; a l'adresse d'un entier. La notation *a fait référence à une variable (ou, plus généralement, une lvalue) appartenant à la procédure appelante.

L'argument effectif correspondant à un tel argument formel doit être une adresse. Souvent, cela est obtenu par l'utilisation de l'opérateur &. Par exemple, si x, y et z sont des variables entières, un appel correct de la fonction précédente sera

Sélectionnez

z = quotient(&x, y);

Bien entendu, l'opérateur & ne doit pas être utilisé si l'argument effectif est déjà une adresse. Le schéma suivant résume les principales situations possibles. Soient les fonctions

Sélectionnez

f(int a) {
    ...
}
g(int *b) {
    ...
}
h(int c, int *d) {
    ...
}

A l'intérieur de la fonction h, des appels corrects de f et g sont :

Sélectionnez

f(c);
f(*d);
g(&c);
g(d);

cette dernière expression correspondant, bien sûr, à g(&*d).

IV-C-4. Arguments en nombre variable

Le langage C permet de définir des fonctions dont le nombre d'arguments n'est pas fixé et peut varier d'un appel à un autre. Nous exposons ici la manière dont cette facilité est réalisée dans le C ANSI. Dans le C original elle existe aussi ; elle consiste à utiliser, sans garde-fou, une faiblesse du langage (la non-vérification du nombre d'arguments effectifs) et une caractéristique du système sous-jacent (le sens d'empilement des arguments effectifs). Mais tout cela est suffisamment casse-cou pour que nous préférions n'expliquer que la manière ANSI de faire, un peu plus fiable.

Une fonction avec un nombre variable d'arguments est déclarée en explicitant quelques arguments fixes, au moins un, suivis d'une virgule et de trois points : « , ... » . Exemple :

Sélectionnez

int max(short n, int x0, ...)

Une telle fonction peut être appelée avec un nombre quelconque d'arguments effectifs, mais il faut qu'il y en ait au moins autant qu'il y a d'arguments formels nommés, ici deux. Exemple d'appel :

Sélectionnez

m = max(3, p, q, r);

Pour les arguments nommés, l'affectation des valeurs des arguments effectifs aux arguments formels est faite comme d'ordinaire. Pour les arguments anonymes elle s'effectue comme pour une fonction sans prototype : les char et les short sont convertis en int, les float en double et il n'y a aucune autre conversion.

A l'intérieur de la fonction on accède aux arguments anonymes au moyen des macros va start et va arg définies dans le fichier stdarg.h, comme le montre l'exemple suivant :

Sélectionnez

#include <stdarg.h>

int max(short n, int x1, ...) {
    va_list magik;
    int x, i, m;
    m = x1;
    va_start(magik, x1);
    for (i = 2; i <= n; i++)
        if ((x = va_arg(magik, int)) > m)
        m = x;
    return m;
}

Des appels corrects de cette fonction seraient :

Sélectionnez

a = max(3, p, q, r);
b = max(8, x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]);
c = max(2, u, 0);

Les éléments définis dans le fichier stdarg.h sont :

va list pointeur Déclaration de la variable pointeur, qui sera automatiquement gérée par le dispositif d'accès aux arguments. Le programmeur choisit le nom de cette variable, mais il n'en fait rien d'autre que la mettre comme argument dans les appels des macros va start et va arg.

va start(pointeur, dernier argument) Initialisation de la variable pointeur ; dernier argument doit être le der- nier des arguments explicitement nommés dans l'en-tête de la fonction.

va arg(pointeur, type) Parcours des arguments anonymes : le premier appel de cette macro donne le premier argument anonyme ; chaque appel suivant donne l'argument suivant le dernier déjà obtenu. Chaque fois, type doit décrire le type de l'argument qui est en train d'être référencé.

Comme l'exemple le montre, le principe des fonctions avec un nombre variable d'arguments est de déclarer effectivement au moins un argument, dont la fonction peut déduire l'adresse des autres. Il faut aussi choisir le moyen d'indiquer à la fonction le nombre de valeurs réellement fournies. Dans l'exemple ci-dessus ce nombre est passé comme premier argument ; une autre technique consiste à utiliser une valeur « intruse » (ex : le pointeur NULL parmi des pointeurs valides). La fonction printf est un autre exemple de fonction avec un nombre variable d'arguments. Elle est déclarée (dans le fichier stdio.h) :

Sélectionnez

int printf(char *, ...);

Ici encore, le premier argument (le format) renseigne sur le nombre et la nature des autres arguments. La question des fonctions formelles (fonctions arguments d'autres fonctions) et d'autres remarques sur les fonctions et leurs adresses sont traitées à la section 6.3.1.


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • V. Objets structurés
    • V-A. Tableaux
      • V-A-1. Cas général
      • V-A-2. Initialisation des tableaux
      • V-A-3. Chaines de caractères
    • V-B. Structures et unions
      • V-B-1. Structures
      • V-B-2. Unions
      • V-B-3. Champs de bits
    • V-C. Enumérations
    • V-D. Déclarateurs complexes
      • V-D-1. Cas des déclarations
      • V-D-2. Pointeurs et tableaux constants et volatils
      • V-D-3. La déclaration typedef
      • V-D-4. Cas des types désincarnés

V. Objets structurés

V-A. Tableaux

V-A-1. Cas général

Dans le cas le plus simple la déclaration d'un tableau se fait par une formule comme :

Sélectionnez

    type-de-base nom [ expressionopt ] ;

Exemple :

Sélectionnez

    unsigned long table[10];

L'expression optionnelle qui figure entre les crochets spécifie le nombre d'éléments du tableau. Le premier élément possède toujours l'indice 0. Par conséquent, un tableau t déclaré de taille N possède les éléments t0, t1, ... tN-1. Ainsi la définition précédente alloue un tableau de 10 éléments, nommés respectivement table[0], table[1], ... table[9].

L'expression optionnelle qui figure entre les crochets doit être de type entier et constante au sens de la section 1.3.4, c'est-à-dire une expression dont tous les éléments sont connus au moment de la compilation. De cette manière le compilateur peut l'évaluer et connaitre la quantité d'espace nécessaire pour loger le tableau. Elle est obligatoire lorsqu'il s'agit d'une définition, car il y a alors allocation effective du tableau. Elle est facultative dans le cas des déclarations qui ne sont pas des définitions, c'est-à-dire :

  • lors de la déclaration d'un tableau qui est un argument formel d'une fonction ;
  • lors de la déclaration d'un tableau externe (défini dans un autre fichier).

Sémantique du nom d'un tableau. En général lorsque le nom d'un tableau apparait dans une expression il y joue le même rôle qu'une constante de type adresse ayant pour valeur l'adresse du premier élément du tableau. Autrement dit, si t est de type tableau, les deux expressions t et &t[0] sont équivalentes.

Les exceptions à cette règle, c'est-à-dire les expressions de type tableau qui ne sont pas équivalentes à l'adresse du premier élément du tableau, sont

  • l'occurrence du nom du tableau dans sa propre déclaration ;
  • l'apparition d'un nom du tableau comme argument de sizeof.

Ce sont les seules occasions ou le compilateur veut bien se souvenir qu'un nom de tableau représente aussi un espace mémoire possédant une certaine taille.

De tout cela, il faut retenir en tout cas que le nom d'un tableau n'est pas une lvalue et donc qu'il n'est pas possible d'affecter un tableau à un autre. Si a et b sont deux tableaux, même ayant des types identiques, l'affectation b = a sera

  • comprise comme l'affectation d'une adresse à une autre, et
  • rejetée, car b n'est pas modifiable.

Tableaux multidimensionnels. C ne prévoit que les tableaux à un seul indice, mais les éléments des tableaux peuvent à leur tour être des tableaux. La déclaration d'un tableau avec un nombre quelconque d'indices suit la syntaxe :

Sélectionnez

    type nom [ expressionopt ] [ expressionopt ] ... [ expressionopt ]

qui est un cas particulier de formules plus générales expliquées à la section 5.4.1. Par exemple, la déclaration

Sélectionnez

    double matrice[10][20];

introduit matrice comme étant le nom d'un tableau de 10 éléments 33 qui, chacun à son tour, est un tableau de 20 éléments de type double.

33Dans le cas d'une matrice, la coutume est d'appeler ces éléments les lignes de la matrice. Cela permet de dire alors : « en C les matrices sont rangées par lignes ».

V-A-2. Initialisation des tableaux

Une variable de type tableau peut être initialisée lors de sa déclaration. Cela se fait par une expression ayant la syntaxe :

Sélectionnez

{ expression , expression , ... expression }

Exemple :

Sélectionnez

int t[5] = { 10, 20, 30, 40, 50 };

Les expressions indiquées doivent être des expressions constantes au sens de la section 1.3.4. S'il y a moins d'expressions que le tableau a d'éléments, les éléments restants sont remplis de zéros (cas des variables globales) ou bien restent indéterminés (cas des variables locales). S'il y a plus d'expressions d'initialisation que d'éléments dans le tableau, une erreur est signalée.

D'autre part, la présence d'un initialisateur dispense d'indiquer la taille du tableau. Il est convenu que ce dernier doit avoir alors pour taille le nombre effectif de valeurs initiales. Par exemple, la définition

Sélectionnez

int t[] = { 10, 20, 30, 40, 50 };

alloue un tableau à 5 composantes garni avec les valeurs indiquées.

Lorsque le tableau comporte des sous-tableaux, c'est-à-dire lorsque c'est un tableau à plusieurs indices, la liste des expressions d'initialisation peut comporter des sous-listes indiquées à leur tour avec des accolades, mais ce n'est pas une obligation. Le cas échéant, la règle de complétion par des zéros s'applique aussi aux sous-listes. Par exemple, les déclarations

Sélectionnez

int t1[4][4] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
int t2[4][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

allouent et garnissent les deux tableaux de la figure 5.

Fig. 5 - Tableaux initialisés
Fig. 5 - Tableaux initialisés

Remarque. Rappelons que ces représentations rectangulaires sont très conventionnelles. Dans la mémoire de l'ordinateur ces deux tableaux sont plutôt arrangés comme sur la figure .

Fig. 6 - Tableaux « à plat »
Fig. 6 - Tableaux « à plat »

Si la taille d'une composante est un diviseur de la taille d'un entier, les tableaux sont « tassés » : chaque composante occupe le moins de place possible. En particulier, dans un tableau de char (resp. de short), chaque composante occupe un (resp. deux) octet(s).

V-A-3. Chaines de caractères

Les chaines de caractères sont représentées comme des tableaux de caractères. Un caractère nul suit le dernier caractère utile de la chaine et en indique la fin. Cette convention est suivie par le compilateur (qui range les chaines constantes en leur ajoutant un caractère nul), ainsi que par les fonctions de la librairie standard qui construisent des chaines. Elle est supposée vérifiée par les fonctions de la librairie standard qui exploitent des chaines. Par conséquent, avant de passer une chaine à une telle fonction, il faut s'assurer qu'elle comporte bien un caractère nul à la fin.

Donnons un exemple classique de traitement de chaines : la fonction strlen (extraite de la bibliothèque standard) qui calcule le nombre de caractères utiles d'une chaine :

Sélectionnez

int strlen(char s[]) {
    register int i = 0;
    while (s[i++] != '\0')
        ;
    return i - 1;
}

Une constante-chaine de caractères apparaissant dans une expression a le même type qu'un tableau de caractères c'est-à-dire, pour l'essentiel, le type « adresse d'un caractère ». Par exemple, si p a été déclaré

Sélectionnez

char *p;

l'affectation suivante est tout à fait légitime :

Sélectionnez

p = "Bonjour. Comment allez-vous?";

Elle affecte à p l'adresse de la zone de la mémoire ou le compilateur a rangé le texte en question (l'adresse du 'B' de "Bonjour..."). Dans ce cas, bien que l'expression *p soit une lvalue (ce qui est une propriété syntaxique), elle ne doit pas être considérée comme telle car *p représente un objet mémorisé dans la zone des constantes, et toute occurrence de *p comme opérande gauche d'une affectation :

Sélectionnez

*p = expression;

constituerait une tentative de modification d'un objet constant. Cette erreur est impossible à déceler à la compilation. Selon le système sous-jacent, soit une erreur sera signalée à l'exécution, provoquant l'abandon immédiat du programme, soit aucune erreur ne sera signalée mais la validité de la suite du traitement sera compromise.

Initialisation. Comme tous les tableaux, les variables-chaines de caractères peuvent être initialisées lors de leur déclaration. De ce qui a été dit pour les tableaux en général il découle que les expressions suivantes sont correctes :

Sélectionnez

char message1[80] 	= { 'S', 'a', 'l', 'u', 't', '\0' };
char message2[] 	= { 'S', 'a', 'l', 'u', 't', '\0' };

(la deuxième formule définit un tableau de taille 6). Heureusement, C offre un raccourci : les deux expressions précédentes peuvent s'écrire de manière tout à fait équivalente :

Sélectionnez

char message1[80] 	= "Salut";
char message2[] 	= "Salut";

Il faut bien noter cependant que l'expression "Salut" qui apparait dans ces deux exemples n'est pas du tout traité par le compilateur comme les autres chaines de caractères constantes rencontrées dans les programmes. Ici, il ne s'agit que d'un moyen commode pour indiquer une collection de valeurs initiales à ranger dans un tableau. A ce propos, voir aussi la remarque 2 de la section 6.2.1.

V-B. Structures et unions

V-B-1. Structures

Les structures sont des variables composées de champs de types différents. Elles se déclarent sous la syntaxe suivante :

Image non disponible

A la suite du mot-clé struct on indique :

  • facultativement, le nom que l'on souhaite donner à la structure. Par la suite, l'expression « struct nom » pourra être employée comme une sorte de nom de type ;
  • facultativement, entre accolades, la liste des déclarations des champs de la structure. Chaque champ peut avoir un type quelconque (y compris struct, bien sûr) ;
  • facultativement, la liste des variables que l'on définit ou déclare comme possédant la structure ici définie.

Exemple.

Sélectionnez

struct fiche {
    int numero;
    char nom[32], prenom[32];
} a, b, c;

Cette déclaration introduit trois variables a, b et c, chacune constituée des trois champs numero, nom et prenom ; en même temps elle donne à cette structure le nom fiche. Plus loin, on pourra déclarer de nouvelles variables analogues à a, b et c en écrivant simplement

Sélectionnez

struct fiche x, y;

(remarquez qu'il faut écrire « struct fiche » et non pas « fiche »). Nous aurions pu aussi bien faire les déclarations :

Sélectionnez

struct fiche {
int numero;
char nom[32], prenom[32];
};
struct fiche a, b, c, x, y;

ou encore

Sélectionnez

struct {
int numero;
char nom[32], prenom[32];
} a, b, c, x, y;

mais cette dernière forme nous aurait empêché par la suite de déclarer aussi facilement d'autres variables de même type.

Comme dans beaucoup de langages, on accède aux champs d'une variable de type structure au moyen de l'opérateur « . ». Exemple :

Sélectionnez

a.numero = 1234;

Les structures supportent les manipulations « globales » 34 :

  • on peut affecter une expression d'un type structure à une variable de type structure (pourvu que ces deux types soient identiques) ;
  • les structures sont passées par valeur lors de l'appel des fonctions ;
  • le résultat d'une fonction peut être une structure.

La possibilité pour une fonction de rendre une structure comme résultat est officielle dans le C ANSI ; sur ce point les compilateurs plus anciens présentent des différences de comportement. D'autre part il est facile de voir que -sauf dans le cas de structures vraiment très simples- les transmettre comme résultats des fonctions est en général peu efficace.

Voici quelques informations sur la disposition des champs des structures. Elles sont sans importance lorsque les structures sont exploitées par le programme qui les crée, mais deviennent indispensables lorsque les structures sont partagées entre sous-programmes écrits dans divers langages ou lorsqu'elles servent à décrire les articles d'un fichier existant en dehors du programme.

Position. C garantit que les champs d'une structure sont alloués dans l'ordre ou ils apparaissent dans la déclaration. Ainsi, avec la définition

Sélectionnez

struct modele {
    type1 a, b;
    type2 c, d, e;
    type3 f;
}

on trouve, dans le sens des adresses croissantes : x.a, x.b, x.c, x.d, x.e et enfin x.f 35. Contiguïté. Les champs des structures subissent en général des contraintes d'alignement dépendant du système sous-jacent, qui engendrent des trous anonymes entre les champs. Il s'agit souvent des mêmes règles que celles qui pèsent sur les variables simples. Deux cas relativement fréquents sont les suivants :

  • les champs de type char sont à n'importe quelle adresse, tous les autres champs devant commencer à une adresse paire ;
  • les champs d'un type simple doivent commencer à une adresse multiple de leur taille (les short à une adresse paire, les long à une adresse multiple de quatre, etc.).

Initialisation. Une variable de type structure peut être initialisée au moment de sa déclaration, du moins dans le cas d'une variable globale. La syntaxe est la même que pour un tableau. Exemple :

Sélectionnez

struct fiche {
int numero;
char nom[32], prenom[32];
} u = { 1234, "DURAND", "Pierre" };

Comme pour les tableaux, si on indique moins de valeurs que la structure ne comporte de champs, alors les champs restants sont initialisés par des zéros. Donner plus de valeurs qu'il n'y a de champs constitue une erreur.

34L'expression correcte de cette propriété consisterait à dire qu'une structure, comme une expression d'un type primitif, bénéficie de la sémantique des valeurs, par opposition à un tableaux qui, lui, est assujetti à la sémantique des valeurs.

35Alors que, avec la déclaration correspondante en Pascal (facile à imaginer), beaucoup de compilateurs alloueraient les champs dans l'ordre : x.b, x.a, x.e, x.d, x.c et enfin x.f.

V-B-2. Unions

Tandis que les champs des structures se suivent sans se chevaucher, les champs d'une union commencent tous au même endroit et, donc, se superposent. Ainsi, une variable de type union peut contenir, à des moments différents, des objets de types et de tailles différents. La taille d'une union est celle du plus volumineux de ses champs.

La syntaxe de la déclaration et de l'utilisation d'une union est exactement la même que pour les structures, avec le mot union à la place du mot struct. Exemple (la struct adresse virt est définie à la section 5.2.3) :

Sélectionnez

union mot_machine {
unsigned long mot;
char *ptr;
struct adresse_virt adrv;
} mix;

La variable mix pourra être vue tantôt comme un entier long, tantôt comme l'adresse d'un caractère, tantôt comme la structure bizarre définie ci-après. Par exemple, le programme suivant teste si les deux bits hauts d'une certaine variable m, de type unsigned long, sont nuls :

Sélectionnez

mix.mot = m;
if (mix.adrv.tp == 0) /* etc. */

Remarque 1. Dans ce genre de problèmes, les opérateurs binaires de bits (&, |, ^) s'avèrent également très utiles. L'instruction précédente pourrait aussi s'écrire (sur une machine 32 bits, ou int est synonyme de long) :

Sélectionnez

if (m & 0xC0000000 == 0) /* etc. */

(l'écriture binaire du chiffre hexa C est 1100). Bien sûr, les programmes de cette sorte n'étant pas portables, ces deux manières de faire peuvent être équivalentes sur une machine et ne pas l'être sur une autre.

Remarque 2. Les unions permettent de considérer un même objet comme possédant plusieurs types ; elles semblent donc faire double emploi avec l'opérateur de conversion de type. Ce n'est pas le cas. D'une part, cet opérateur ne supporte pas les structures, tandis qu'un champ d'une union peut en être une.

D'autre part, lorsque l'on change le type d'un objet au moyen de l'opérateur de changement de type, C convertit la donnée, afin que l'expression ait, sous le nouveau type, une valeur vraisemblable et utile. Par exemple, si x est une variable réelle (float), l'expression « (int) x » convertit x en l'entier dont la valeur est la plus voisine de la valeur qu'avait le réel x. Cela implique une transformation, éventuellement assez coûteuse, de la valeur de x. A l'opposé, lorsqu'on fait référence à un champ d'une union, la donnée ne subit aucune transformation, même si elle a été affectée en faisant référence à un autre champ. Par exemple, si l'on déclare

Sélectionnez

union {
float reel;
int entier;
} x;

et qu'ensuite on exécute les affectations :

Sélectionnez

x.reel = 12.5;
i = x.entier;

l'entier i obtenu sera celui dont la représentation binaire coïncide avec la représentation binaire du nombre flottant 12.5 ; cet entier n'a aucune raison d'être voisin de 12.

V-B-3. Champs de bits

Le langage C permet aussi d'utiliser des structures et des unions dont les champs, tous ou seulement certains, sont faits d'un nombre quelconque de bits. De tels champs doivent être ainsi déclarés :

Image non disponible

Chaque constante précise le nombre de bits qu'occupe le champ. Le type doit obligatoirement être entier. Lorsque le type et le nom du champ sont absents, le nombre indiqué de bits est quand même réservé : on suppose qu'il s'agit de bits de « rembourrage » entre deux champs nommés.

Chacun de ces champs est logé immédiatement après le champ précédent, sauf si cela le met à cheval sur deux mots (de type int) ; dans ce cas, le champ est logé au début du mot suivant.

Exemple : la structure

Sélectionnez

struct adresse_virt {
    unsigned depl : 9;
    unsigned numpagv : 16;
: 5;
unsigned tp : 2;
};

découpe un mot de 32 bits en en quatre champs, comme indiqué 36 sur la figure 7.

Fig. 7 - Champs de bits
Fig. 7 - Champs de bits

Attention. Dans tous les cas, les champs de bits sont très dépendants de l'implantation. Par exemple, certaines machines rangent les champs de gauche à droite, d'autres de droite à gauche. Ainsi, la portabilité des structures contenant des champs de bits n'est pas assurée ; cela limite leur usage à une certaine catégorie de programmes, comme ceux qui communiquent avec leur machine-hote à un niveau très bas (noyau d'un système d'exploitation, gestion des périphériques, etc.). Ces programmes sont rarement destinés à être portés d'un système à un autre.

36Sur un système où un mot est fait de 32 bits (sinon un champ serait à cheval sur deux mots, ce qui est interdit).

V-C. Enumérations

Les énumérations ne constituent pas un type structuré. Si elles figurent ici c'est uniquement parce que la syntaxe de leur déclaration possède des points communs avec celle des structures. Dans les cas simples, cette syntaxe est la suivante :

Image non disponible

ou chaque id-valeur est à son tour de la forme

Image non disponible

Exemple :

Sélectionnez

enum jourouvrable { lundi, mardi, mercredi, jeudi, vendredi } x, y;

A la suite d'une telle déclaration, le type enum jourouvrable est connu ; les variables x et y le possèdent. Ce type est formé d'une famille finie de symboles qui représentent des constantes entières : lundi (égale à 0), mardi (égale à 1), etc. Dans ce programme on pourra écrire :

Sélectionnez

enum jourouvrable a, b, c;

On aurait aussi bien pu introduire toutes ces variables par les déclarations

Sélectionnez

enum jourouvrable { lundi, mardi, mercredi, jeudi, vendredi };
...
enum jourouvrable x, y, a, b, c;
ou encore
enum { lundi, mardi, mercredi, jeudi, vendredi } x, y, a, b, c;

même type. Bien sûr, les énumérations peuvent se combiner avec la déclaration typedef (cf. section V.D.3) : après la déclaration

Sélectionnez

typedef enum jourouvrable { lundi, mardi, mercredi, jeudi, vendredi } JOUROUVRABLE;

les expressions « enum jourouvrable » et « JOUROUVRABLE » sont équivalentes.

Les nombres entiers et les valeurs de tous les types énumérés sont totalement compatibles entre eux. Par conséquent, l'affectation d'un entier à une variable d'un type énuméré ou l'affectation réciproque ne provoquent en principe aucun avertissement de la part du compilateur . Ainsi, en C, les types énumérés ne sont qu'une deuxième manière de donner des noms à des nombres entiers (la première manière étant l'emploi de la directive #define, voir section 8.1.2).

Par défaut, la valeur de chacune de ces constantes est égale au rang de celle-ci dans l'énumération (lundi vaut 0, mardi vaut 1, etc.). On peut altérer cette séquence en associant explicitement des valeurs à certains éléments :

Sélectionnez

enum mois_en_r { janvier = 1, fevrier, mars, avril, septembre = 9, octobre, novembre, decembre };

(fevrier vaut 2, mars vaut 3, octobre vaut 10, etc.)

V-D. Déclarateurs complexes

Jusqu'ici nous avons utilisé des formes simplifiées des déclarations. Tous les objets étaient soit simples, soit des tableaux, des fonctions ou des adresses d'objets simples. Il nous reste à voir comment déclarer des « tableaux de pointeurs » , des « tableaux d'adresses de fonctions », des « fonctions rendant des adresses de tableaux », etc.

Autrement dit, nous devons expliquer la syntaxe des formules qui permettent de décrire des types de complexité quelconque.

La question concerne au premier chef les descripteurs de types, qui apparaissent dans trois sortes d'énoncés :

  • les définitions de variables (globales et locales), les déclarations des paramètres formels des fonctions, les déclarations des variables externes et les déclarations des champs des structures et des unions ;
  • les définitions des fonctions et les déclarations des fonctions externes ;
  • les déclarations de types (typedef)

La même question réapparait dans des constructions qui décrivent des types sans les appliquer à des identificateurs. Appelons cela des types désincarnés. Ils sont utilisés

  • par l'opérateur de changement de type lorsque le type de destination n'est pas défini par un identificateur : « (char *) expression » ;
  • comme argument de sizeof (rare) : « sizeof(int [100]) » ;
  • dans les prototypes des fonctions : « strcpy(char *, char *) ; »

V-D-1. Cas des déclarations

Un déclarateur complexe se compose de trois sortes d'ingrédients :

  • un type « terminal » qui est un type de base (donc numérique), enum, struct ou union ou bien un type auquel on a donné un nom par une déclaration typedef ;
  • un certain nombre d'occurrences des opérateurs (), [] et * qui représentent des procédés récursifs de construction de types complexes ;
  • l'identificateur qu'il s'agit de déclarer.

Ces formules ont mauvaise réputation. Si elles sont faciles à lire et à traiter par le compilateur, elles s'avèrent difficiles à composer et à lire par le programmeur, car les symboles qui représentent les procédés récursifs de construction sont peu expressifs et se retrouvent placés à l'envers de ce qui aurait semblé naturel.

Pour cette raison nous allons donner un procédé mécanique qui, à partir d'une sorte de description « à l'endroit », facile à concevoir, fabrique la déclaration C souhaitée. Appelons description naturelle d'un type une expression de l'un des types suivants :

  • la description correcte en C d'un type de base, d'un type enum, d'une struct ou union ou le nom d'un type baptisé par typedef ;
  • la formule « tableau de T », ou T est la description naturelle d'un type ;
  • la formule « adresse d'un T » (ou « pointeur sur un T »), ou T est la description naturelle d'un type ;
  • la formule « fonction rendant un T », ou T est la description naturelle d'un type.

Dans ces conditions, nous pouvons énoncer la « recette de cuisine » du tableau 3.

Tab. 3 - Construction d'un déclarateur complexe
Tab. 3 - Construction d'un déclarateur complexe

Exemple 1 : soit à déclarer tabPtr comme un tableau (de dimension indéterminée) de pointeurs d'entiers.

Appliquant les règles du tableau 3, nous écrirons successivement :

Sélectionnez

tableau de pointeur sur int 		tabPtr
pointeur sur int 			(tabPtr)[]
pointeur sur int 			tabPtr[]
int 					*tabPtr[]

La partie gauche de la ligne ci-dessus se réduisant à un type simple, cette ligne constitue la déclaration C cherchée, comme il faudra l'écrire dans un programme : « int *tabPtr[] ; ».

Exemple 2 : pour la comparer à la précédente, cherchons à écrire maintenant la déclaration de ptrTab, un pointeur sur un tableau 37 d'entiers :

Sélectionnez

pointeur sur un tableau de int ptrTab
tableau de int *ptrTab
int (*ptrTab)[]

Les parenthèses ci-dessus ne sont pas facultatives, car elles encadrent une expression qui commence par * (dit autrement, elles sont nécessaires, car [] a une priorité supérieure à *).

Les déclarations pour lesquelles le membre gauche est le même (à la fin des transformations) peuvent être regroupées. Ainsi, les déclarations de tabPtr et ptrTab peuvent être écrites ensemble :

Sélectionnez

int *tabPtr[], (*ptrTab)[];

En toute rigueur les règles a et b de ce tableau devraient être plus complexes, car la description naturelle du type d'une fonction inclut en général la spécification des types des arguments (le prototype de la fonction), et la description naturelle d'un type tableau comporte l'indication du nombre de composantes de celui-ci.

En réalité ces indications s'ajoutent aux règles données ici, presque sans modifier le processus que ces règles établissent : tout simplement, des informations annexes sont écrites à l'intérieur des crochets des tableaux et des parenthèses des fonctions.

Exemple 3 : soit à déclarer une matrice de 10 lignes et 20 colonnes. Nos pouvons écrire successivement :

Sélectionnez

tableau de 10 tableau de 20 double 		matrice
tableau de 20 double 				(matrice)[10]
tableau de 20 double 				matrice[10]
double 						(matrice[10])[20]
double 						matrice[10][20]

Exemple 4 : quand on écrit des programmes relevant du calcul numérique on a souvent besoin de variables de type « adresse d'une fonction R → R ». Voici comment écrire un tel type

Sélectionnez

adresse de fonction (avec argument double) rendant double 		adrFon
fonction (avec argument double) rendant double 				*adrFon
double 									(*adrFon)(double)

La déclaration cherchée est donc « double (*adrFon)(double) ».

Exemple 5 : cet exemple abominable est extrait de la bibliothèque UNIX. La fonction signal renvoie l'adresse d'une « procédure », c'est-à-dire d'une fonction rendant void. Voici comment il faudrait la déclarer en syntaxe originale, c'est-à-dire sans spécifier les arguments de la fonction signal (cette déclaration est fournie dans le fichier <signal.h>) :

Sélectionnez

fonction rendant un pointeur sur fonction rendant void			signal
pointeur sur fonction rendant un void					(signal)()
pointeur sur fonction rendant un void 					signal()
fonction rendant un void 						*signal()
void 									(*signal())()

En syntaxe ANSI il faut en plus donner les types des arguments des fonctions. On nous dit que les arguments de la fonction signal sont un int et un pointeur sur une fonction prenant un int et rendant un void. En outre, signal rend un pointeur vers une telle fonction. Nous savons déjà écrire le type « pointeur sur une fonction prenant un int et rendant un void » (ce n'est pas très différent de l'exemple précédent) :

Sélectionnez

void (*fon)(int);

Attaquons-nous à signal :

Sélectionnez

fonction (avec arguments int sig et int (*fon)(int))
    rendant un pointeur sur fonction
    (avec argument int) rendant void 				signal
pointeur sur fonction (avec argument int) rendant void
                                    (signal)(int sig, int (*fon)(int))
pointeur sur fonction (avec argument int) rendant void
                                    signal(int sig, int (*fon)(int))
fonction (avec argument int s) rendant void 				*signal(int sig, int (*fon)(int))
void 									(*signal(int sig, int (*fon)(int)))(int)

Finalement, la déclaration cherchée est

Sélectionnez

void (*signal(int sig, int (*fon)(int)))(int) ;

Comme annoncé, les expressions finales obtenues sont assez lourdes à digérer pour un humain. A leur décharge on peut tout de même dire qu'une fois écrites elles se révèlent bien utiles puisqu'elles font apparaitre les types de leurs éléments terminaux comme ils seront employés dans la partie exécutable des programmes. Ainsi, à la vue de l'expression

Sélectionnez

float matrice[10][20];

on comprend sans effort supplémentaire qu'un élément de la matrice s'écrira matrice[i][j] et que cette formule représentera un objet de type float. De même, l'énoncé

Sélectionnez

void (*signal(sig, func))();

nous fera reconnaitre immédiatement l'expression (*signal(2,f))(n) comme un appel correct de la « procédure » rendue par l'appel de signal.

37La notion de pointeur sur un tableau est assez académique, puisqu'une variable de type tableau représente déjà l'adresse de celui-ci (dans la notion d'« adresse d'un tableau » il y a une double indirection). En pratique on n'en a presque jamais besoin.

V-D-2. Pointeurs et tableaux constants et volatils

Les qualifieurs const et volatile peuvent aussi apparaitre dans les déclarateurs complexes. Plutôt que de rendre encore plus lourdes des explications qui le sont déjà beaucoup, contentons-nous de montrer des exemples significatifs. Les cas les plus utiles sont les suivants :

  1. La formule :
    Sélectionnez

const type *nom

    déclare nom comme un pointeur vers un objet constant ayant le type indiqué (la variable pointée, *nom, ne doit pas être modifiée).

  2. La formule
    Sélectionnez

type *const nom

    pas être modifiée).

  3. Plus simplement, la formule
    Sélectionnez

const type nom[expropt]

    déclare nom comme un tableau d'objets constants 38 (nom[i] ne doit pas être modifié).

Ainsi par exemple les déclarations

Sélectionnez

int e, *pe, *const pce = &e, te[100];
const int ec = 0, *pec, *const pcec = &ec, tec[100];

déclarent successivement :

  • e : un entier
  • pe : un pointeur vers un entier
  • pce : un pointeur constant vers un entier (qui doit être obligatoirement initialisé, puisqu'il est constant)
  • te : un tableau de 100 entiers
  • ec : un entier constant
  • pec : un pointeur vers un entier constant
  • pcec : un pointeur constant vers un entier constant
  • tec : un tableau de 100 entiers constants

Toute affectation avec pour membre gauche pce, ec, *pec, pcec, *pcec ou tec[i] est illégale.

En résumé, la seule vraie nouveauté introduite ici est la notion de pointeur constant et la syntaxe de sa déclaration « type-pointé *const pointeur ». Signalons enfin que toute cette explication reste valable en remplaçant le mot const et la notion d'objet constant par le mot volatile et le concept d' objet volatil (cf. section I.E.7). On a donc la possibilité de déclarer des tableaux d'objets volatils, des pointeurs volatils vers des objets, volatils ou non, etc.

38Notez qu'il n'y a pas besoin d'une notation spéciale pour déclarer un tableau constant d'objets (variables) puisque la signification ordinaire des tableaux est exactement celle-là.

V-D-3. La déclaration typedef

Cette construction est l'homologue de la déclaration type du langage Pascal. Elle permet de donner un nom à un type dans le but d'alléger par la suite les expressions ou il figure. Syntaxe :

Sélectionnez

typedef déclaration

Pour déclarer un nom de type, on fait suivre le mot réservé typedef d'une expression identique à une déclaration de variable, dans laquelle le rôle du nom de la variable est joué par le nom du type qu'on veut définir. Exemple : les déclarations

Sélectionnez

    typedef float MATRICE[10][20];
    ...
    MATRICE mat;

sont équivalentes à

Sélectionnez

    float mat[10][20];

Voici un autre exemple :

Sélectionnez

typedef struct noeud {
    int info;
    struct noeud *fils_gauche, *fils_droit;
} NOEUD;

Cette expression déclare l'identificateur NOEUD comme le nom du type « la structure noeud » . Des variables a, b, c de ce type pourront par la suite être déclarées aussi bien par l'expression

Sélectionnez

struct noeud a, b, c;

que par

Sélectionnez

NOEUD a, b, c;

Remarquez l'utilisation différente d'un nom de structure (précédé du mot struct) et d'un nom de type. Par tradition, les noms des types définis à l'aide de la déclaration typedef sont écrits en majuscules. L'ensemble des règles de syntaxe qui s'appliquent aux déclarations de variables s'appliquent également ici. Par exemple, on peut introduire plusieurs noms de types dans un seul énoncé typedef. Ainsi, à la suite de

Sélectionnez

typedef struct noeud {
    int info;
    struct noeud *fils_gauche, *fils_droit;
} NOEUD, *ARBRE;

les identificateurs NOEUD et ARBRE sont des synonymes pour « struct noeud » et « struct noeud * ». Dans la construction des déclarateurs complexes, le nom d'un type qui a fait l'objet d'une déclaration typedef peut être utilisé comme type de base. Par exemple :

Sélectionnez

typedef float LIGNE[20];
typedef LIGNE MATRICE[10];

est une autre manière d'introduire le type MATRICE déjà vu. De même

Sélectionnez

typedef void PROCEDURE(int);
PROCEDURE *signal(int sig, PROCEDURE *func);

est une autre manière (nettement plus facile à lire, n'est-ce pas ?) de déclarer la fonction signal vue précédemment.

V-D-4. Cas des types désincarnés

Tout cela devient encore plus ésotérique lorsqu'on doit construire un type sans l'appliquer à un identificateur. Le principe est alors le suivant : construisez la déclaration correcte d'un nom quelconque X, puis effacez X. De telles expressions sont utiles dans trois circonstances :

  1. Pour construire un opérateur de changement de type. Exemple : la fonction malloc alloue un bloc de mémoire de taille donnée, dont elle renvoie l'adresse. Dans les bibliothèques du C original , cette fonction est déclarée comme rendant l'adresse d'un char, et l'utilisateur doit convertir le résultat de malloc dans le type qui l'intéresse. Supposons avoir besoin d'espace pour ranger un tableau de N réels en double précision.

    Déclarations 40 :

    Sélectionnez

char *malloc(size_t);
...
double *p;

    Utilisation :

    Sélectionnez

p = (double *) malloc(N * sizeof(double)); /* DéCONSEILLé EN C ANSI */

    Le type désincarné « double * » apparaissant ci-dessus a été déduit d'une déclaration de pointeur, « double *ptr » en effaçant le nom déclaré, ptr.

    Rappelons qu'en C ANSI l'expression ci-dessus s'écrit plus simplement (voyez à la section 2.2.12, notamment à la page 26, des indications et mises en garde à propos de l'opérateur de changement de type) :

    Sélectionnez

p = malloc(N * sizeof(double));
  2. Comme argument de l'opérateur sizeof. Essayons par exemple d'écrire autrement l'affectation précédente. Déclarons un tableau T de N doubles :
    Sélectionnez

double T[N]

    Supprimons T, il reste « double [N] ». Par conséquent, l'appel de la fonction malloc montré ci-dessus peut s'écrire aussi 41 :

    Sélectionnez

p = (double *) malloc(sizeof(double [N]));
  3. Dans les prototypes des fonctions, en C ANSI. Par exemple, la bibliothèque standard C comporte la fonction qsort, qui implémente une méthode de tri rapide d'un tableau (algorithme Quicksort) programmé en toute généralité. Les arguments de cette fonction sont le tableau à trier, le nombre de ses éléments, la taille de chacun et une fonction qui représente la relation d'ordre utilisée (le « critère de tri ») : elle reçoit deux adresses d'éléments du tableau et rend un nombre négatif, nul ou positif exprimant la comparaison.

    La fonction qsort est donc ainsi déclarée dans le fichier stdlib.h :

    Sélectionnez

void qsort(const void*, size_t, size_t, int (*)(const void*, const void*));

Note. Ce n'est pas requis par la syntaxe mais, lorsqu'ils sont bien choisis, les noms des arguments sont une aide pour le programmeur. Exemple :

Sélectionnez

void qsort(const void *tableau, size_t nombre, size_t taille,
    int (*compare)(const void *adr1, const void *adr2));

39Dans la bibliothèque ANSI (fichier stdlib.h) la fonction malloc est déclarée comme rendant une valeur de type « void * ». Ce type étant compatible avec tous les autres types pointeur, le problème exposé ici ne se pose pas.

40Le type size t (défini dans <stddef.h>) représente un entier sans signe

41L'exemple montré ici, « sizeof(double [N]) », est franchement ésotérique. La notation « N * sizeof(double) » est bien plus raisonnable.


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • VI. Pointeurs
    • VI-A. Généralités
      • VI-A-1. Déclaration et initialisation des pointeurs
      • VI-A-2. Les pointeurs génériques et le pointeur NULL
    • VI-B. Les pointeurs et les tableaux
      • VI-B-1. Arithmétique des adresses, indirection et indexation
      • VI-B-2. Tableaux dynamiques
      • VI-B-3. Tableaux multidimensionnels
      • VI-B-4. Tableaux multidimensionnels dynamiques
      • VI-B-5. Tableaux de chaines de caractères
      • VI-B-6. Tableaux multidimensionnels formels
      • VI-B-7. Tableaux non nécessairement indexés à partir de zéro
      • VI-B-8. Matrices non dynamiques de taille inconnue
    • VI-C. Les adresses des fonctions
      • VI-C-1. Les fonctions et leurs adresses
      • VI-C-2. Fonctions formelles
      • VI-C-3. Tableaux de fonctions
      • VI-C-4. Flou artistique
    • VI-D. Structures récursives
      • VI-D-1. Déclaration
      • VI-D-2. Exemple
      • VI-D-3. Structures mutuellement récursives

VI. Pointeurs

VI-A. Généralités

Un pointeur est une variable dont la valeur est l'adresse d'une cellule de la mémoire.

Les premiers langages évolués ne permettaient pas les manipulations de pointeurs, de telles opérations étant obligatoirement confinées dans les programmes écrits en assembleur. Le langage Pascal a introduit les pointeurs parmi les types de données disponibles dans des langages généralistes. Mais ils y sont présentés comme des entités fort mystérieuses, dont l'utilisateur n'est censé connaitre ni la structure ni le comportement. Cela n'en facilite pas le maniement aux programmeurs débutants. Beaucoup des difficultés à comprendre les pointeurs proviennent plus du secret qui les entoure que de leur réelle difficulté d'emploi.

Un pointeur n'est pas un concept unique, sans ressemblance avec les autres types de données, car une adresse n'est en fin de compte rien d'autre qu'un nombre entier (l'indice d'un élément du tableau qu'est la mémoire de l'ordinateur). Un pointeur devrait donc supporter toutes les opérations qu'on peut faire subir à un nombre et qui gardent un sens lorsque ce nombre exprime un rang.

Ce qui est tout à fait spécifique d'un pointeur p est la possibilité de mentionner la variable dont la valeur de p est l'adresse. Un pointeur correctement initialisé est toujours le renvoi à une deuxième variable, la variable pointée, accessible à partir du contenu de p.

Dans tous les langages ou ils existent, le principal intérêt des pointeurs réside dans la possibilité de réaliser des structures de données récursives (listes et arbres) : des structures dans lesquelles un champ est virtuellement aussi complexe que la structure elle-même. En plus de cet aspect, en C les pointeurs sont le moyen d'améliorer l'accès aux éléments des tableaux ordinaires, en incorporant au langage des techniques d'adressage indirect couramment utilisées en assembleur.

VI-A-1. Déclaration et initialisation des pointeurs

Nous avons étudié à la section précédente la syntaxe des déclarations des pointeurs. Dans le cas le plus simple, cette syntaxe est

Sélectionnez

    type *variable

Si par la suite on affecte à une telle variable l'adresse d'un certain objet, on fait référence à ce dernier par l'expression :

Sélectionnez

        *variable

Par exemple, l'expression

Sélectionnez

    char *p;

définit la variable p comme « pointeur vers un char ». Lorsqu'elle aura été correctement initialisée, elle contiendra l'adresse d'un caractère, auquel on pourra faire référence par l'expression

Sélectionnez

*p

Initialisation des pointeurs. Un pointeur contient en principe une adresse valide. Il existe trois manières sûres de donner une telle valeur à un pointeur :

  1. Prendre l'adresse d'une variable existant par ailleurs. Exemple, déclaration :
    Sélectionnez

type x, *p;

    Initialisation :

    Sélectionnez

p = &x;

    p contient maintenant une adresse valide, l'adresse de la variable x. Par la suite, les expressions x et *p désigneront le même objet (le contenu de x).

  2. Provoquer l'allocation d'un nouvel espace, c'est-à-dire allouer dynamiquement une variable anonyme.

    Exemple, déclaration :

    Sélectionnez

type *p;
Initialisation :
p = malloc(sizeof(type));

    Après cette instruction, une variable tout à fait analogue à la variable x de l'exemple précédent existe. Comme dans l'exemple précédent, *p en désigne le contenu. La différence est qu'ici elle n'a pas de nom propre : si la valeur de p venait à être altérée (sans avoir été auparavant sauvegardée dans un autre pointeur), la variable pointée deviendrait inaccessible et serait définitivement perdue.

  3. Obtenir une valeur par des calculs légitimes sur des pointeurs. Par exemple, comme on le verra bientôt, avec les déclarations
    Sélectionnez

type t[10], *p, *q;

    suivies de l'initialisation

    Sélectionnez

p = &t[0]; (ou tout simplement p = t; )

    l'expression

    Sélectionnez

q = p + 3;

    affecte à q l'adresse du quatrième élément du tableau t, c'est-à-dire &t[3].

Il existe aussi une manière risquée, en tout cas non portable, de donner une valeur à un pointeur : lui affecter une constante ou une expression numérique, dans le style de :

Sélectionnez

p = (struct machin *) 0x2A3E;

De telles expressions, indispensables dans certaines fonctions de bas niveau (contrôle d'organes matériels, etc.), rendent non portables les programmes ou elles figurent. Elles doivent donc être évitées chaque fois que cela est possible.

A propos de la taille des pointeurs et celle des entiers. Une expression comme l'affectation ci-dessus soulève un autre problème : existe-t-il une relation entre la taille d'un nombre entier (types int et unsigned int) et celle d'un pointeur ? Le langage ne prescrit rien à ce sujet ; il est donc parfois erroné et toujours dangereux de supposer qu'un entier puisse contenir un pointeur. Cette question peut sembler très secondaire, l'utilisation de variables entières pour conserver ou transmettre des pointeurs paraissant bien marginale. Or c'est ce qu'on fait, peut-être sans s'en rendre compte, lorsqu'on appelle une fonction qui renvoie un pointeur sans avoir préalablement déclaré la fonction. Par exemple, supposons que sans avoir déclaré la fonction malloc (ni inclus le fichier <stdlib.h>) on écrive l'affectation

Sélectionnez

p = (struct machin *) malloc(sizeof(struct machin));

Le compilateur rencontrant malloc pour la première fois, il postule que cette fonction renvoie un int et insère à cet endroit le code qui convertit un entier en un pointeur. Cette conversion peut fournir un résultat tout à fait erroné. Notamment, sur un système ou les entiers occupent deux octets et les pointeurs quatre, les deux octets hauts du résultat de malloc seront purement et simplement remplacés par zéro. La question est d'autant plus vicieuse que le programme ci-dessus se comportera correctement sur tous les systèmes dans lesquels les entiers et les pointeurs ont la même taille.

VI-A-2. Les pointeurs génériques et le pointeur NULL

Certaines fonctions de la bibliothèque, comme malloc, rendent comme résultat une « adresse quelconque », à charge pour l'utilisateur de la convertir en l'adresse spécifique qui lui convient. La question est : comment déclarer de tels pointeurs peu typés ou pas typés du tout ?

Le C original n'ayant rien prévu à ce sujet, la tradition s'était créée de prendre le type char * pour « pointeur vers n'importe quoi ». En effet, sur beaucoup de systèmes les adresses des objets de taille non unitaire supportent des contraintes d'alignement (les short aux adresse paires, les long aux adresses multiples de quatre, etc.), alors que les adresses des caractères sont les seules à ne subir aucune restriction.

Le C ANSI introduit un type spécifique pour représenter de telles adresses génériques : le type void *. Ce type est compatible avec tout autre type pointeur : n'importe quel pointeur peut être affecté à une variable de type void * et réciproquement 42. Par exemple, le fichier standard <stdlib.h> contient la déclaration suivante

Sélectionnez

de la fonction malloc :
void *malloc(size_t size);

Appel :

Sélectionnez

p = malloc(sizeof(struct machin));

Note. A la section 2.2.12 (page 26) nous expliquons pourquoi l'emploi d'un opérateur de changement de type en association avec malloc est dangereux et déconseillé, en C ANSI.

Le pointeur NULL. Une des principales applications des pointeurs est la mise en œvre de structures récursives de taille variable : listes chainées de longueur inconnue, arbres de profondeur quelconque, etc. Pour réaliser ces structures il est nécessaire de posséder une valeur de pointeur, distincte de toute adresse valide, représentant la fin de liste, la structure vide, etc.

C garantit qu'aucun objet valide ne se trouve à l'adresse 0 et l'entier 0 est compatible avec tout type pointeur. Cette valeur peut donc être utilisée comme la valeur conventionnelle du « pointeur qui ne pointe vers rien » ; c'est l'équivalent en C de la constante nil de Pascal. En pratique on utilise plutôt la constante NULL qui est définie dans le fichier <stddef.h> :

Sélectionnez

#define NULL ((void *) 0)

42Avec une protection supplémentaire : le type void étant incompatible avec tous les autres types, l'objet pointé par une variable déclarée de type void * ne pourra pas être utilisé tant que la variable n'aura pas été convertie en un type pointeur précis.

VI-B. Les pointeurs et les tableaux

VI-B-1. Arithmétique des adresses, indirection et indexation

Arithmétique. Certaines des opérations arithmétiques définies sur les nombres gardent un sens lorsque l'un des opérandes ou les deux sont des adresses :

  • l'addition et la soustraction d'un nombre entier à une adresse ;
  • la soustraction de deux adresses (de types rigoureusement identiques).

L'idée de base de ces extensions est la suivante : si exp exprime l'adresse d'un certain objet O1 alors exp+1 exprime l'adresse de l'objet de même type que O1 qui se trouverait dans la mémoire immédiatement après O1 et exp¡1 l'adresse de celui qui se trouverait immédiatement avant.

De manière analogue, si exp1 et exp2 sont les adresses respectives de deux objets O1 et O2 de même type et contigus (O2 après O1), il semble naturel de donner à exp2-exp1 la valeur 1.

L'arithmétique des adresses découle de ces remarques. D'un point de vue technique, ajouter 1 à un pointeur c'est le considérer momentanément comme un entier et lui ajouter une fois la taille de l'objet pointé. On peut aussi dire cela de la manière suivante : exp1 et exp2 étant deux expressions de type « adresse d'un objet de type T » et n une expression entière, nous avons :

Sélectionnez

exp1 + n ≡ (T *) ((unsigned long) exp1 + n*sizeof(T))

et

Sélectionnez

exp2 - exp1 ≡ ((unsigned long) exp2 - (unsigned long) exp1) / sizeof(T)

L'indexation. Il découle des explications précédentes que l'arithmétique des adresses ne se justifie que si on peut considérer un pointeur p comme l'adresse d'un élément t[i0] d'un tableau, éventuellement fictif. On peut alors interpréter p + i comme l'adresse de t[i0 + i] et q - p comme l'écart entre les indices correspondant à ces deux éléments. Les pointeurs avec leur arithmétique et l'accès aux éléments des tableaux sont donc des concepts très voisins. En réalité il y a bien plus qu'un simple lien de voisinage entre ces deux concepts, car l'un est la définition de l'autre :

Soit exp une expression quelconque de type adresse et ind une expression entière. Par définition, l'expression :

Sélectionnez

*(exp + ind)

se note :

Sélectionnez

exp[ind]

On en déduit que si on a l'une ou l'autre des déclarations

Sélectionnez

type t[100];

ou

Sélectionnez

type *t;

alors dans un cas comme dans l'autre on peut écrire indifféremment

Sélectionnez

t[0] ou *t

ou

Sélectionnez

t[i] ou *(t + i)

Bien entendu, un programme est plus facile à lire si on montre une certaine constance dans le choix des notations.

Exemple. Le programme suivant parcourt un tableau en utilisant un pointeur :

Sélectionnez

char s[10] = "Hello";
main() {
char *p = s;
while (*p != '\0')
fputc(*p++, stdout);
}

Remarque 1. La similitude entre un tableau et un pointeur est grande, notamment dans la partie exécutable des fonctions. Mais il subsiste des différences, surtout dans les déclarations. Il ne faut pas oublier que les déclarations

Sélectionnez

int *p, t[100];

introduisent p comme une variable pointeur et t comme une constante pointeur. Ainsi, les expressions p = t et p++ sont légitimes, mais t = p et t++ sont illégales, car elles tentent de modifier une « non variable » (un nom de tableau n'est pas une lvalue).

Remarque 2. Dans ce même esprit, rappelons la différence qu'il peut y avoir entre les deux déclarations suivantes :

Sélectionnez

char s[] = "Bonsoir";

et

Sélectionnez

char *t = "Bonsoir";

s est (l'adresse d') un tableau de caractères (figure 8)

Fig. 8 - Chaine de caractères « variable »
Fig. 8 - Chaine de caractères « variable »

tandis que t est l'adresse d'un (tableau de) caractère(s) (figure 9)

Fig. 9 - Constante chaine de caractères
Fig. 9 - Constante chaine de caractères

On pourra référencer ces caractères indifféremment par s[i], *(s + i), t[i] ou *(t + i). Cependant, dans le premier cas le compilateur a alloué un vrai tableau de 8 caractères, et y a copié la chaine donnée ; dans le second cas, la chaine a été rangée avec les autres constantes littérales et le compilateur n'a alloué qu'une variable de type pointeur dans laquelle il a rangé l'adresse de cette constante. Par suite, s n'est pas une variable mais *s, s[i] et *(s + i) en sont, tandis que t est une variable mais *t, t[i] et *(t + i) n'en sont pas 43.

A ce propos voir aussi la section 5.1.3.

L'arithmétique des adresses permet d'améliorer l'efficacité de certains traitements de tableaux 44, en mettant à profit le fait que l'indirection est plus rapide que l'indexation. Autrement dit, l'accès à une donnée à travers une expression de la forme

Sélectionnez

*p

est réputée (légèrement) plus rapide que l'accès à la même donnée à travers l'expression

Sélectionnez

*(q + i) (ou, ce qui est la même chose, q[i])

Développons un exemple classique : la fonction strcpy(dest, srce) copie la chaine de caractères srce dans l'espace pointé par dest.

Première forme :

Sélectionnez

char *strcpy(char dest[], char srce[]) {
register int i = 0;
while ((dest[i] = srce[i]) != '\0')
i++;
return dest;
}

Deuxième forme :

Sélectionnez

char *strcpy(char *dest, char *srce) {
register char *d = dest, *s = srce;
while ((*d++ = *s++) != '\0')
;
return dest;
}

La deuxième forme est plus efficace, car on y a supprimé l'indexation. Pour s'en convaincre, il suffit de considérer que la boucle qui figure dans la première forme équivaut à :

Sélectionnez

while ((*(dest + i) = *(srce + i)) != '\0')
i++;

43C'est uniquement d'un point de vue technique que ces expressions ne sont pas des variables car du point de vue de la syntaxe elles sont des lvalue tout à fait légitimes et le compilateur ne nous interdira pas de les faire apparaitre à gauche d'une affectation.

44Bien sûr, il s'agit de petites économies dont le bénéfice ne se fait sentir que lorsqu'elles s'appliquent à des parties des programmes qui sont vraiment critiques, c'est-à-dire exécutées de manière très fréquente.

VI-B-2. Tableaux dynamiques

L'équivalence entre les tableaux et les pointeurs permet de réaliser des tableaux dynamiques, c'est-à-dire des tableaux dont la taille n'est pas connue au moment de la compilation mais uniquement lors de l'exécution, lorsque le tableau commence à exister.

Pour cela il suffit de

  • remplacer la déclaration du tableau par celle d'un pointeur ;
  • allouer l'espace à l'exécution, avant toute utilisation du tableau, par un appel de malloc ;
  • dans le cas d'un tableau local, libérer l'espace à la fin de l'utilisation.

Pour tout le reste, l'utilisation de ces tableaux dynamiques est identique à celle des tableaux normaux (ce qui, pour le programmeur, est une très bonne nouvelle !). Exemple : la fonction ci-dessous calcule dans un tableau la Neme ligne du triangle de Pascal 45 et s'en sert dans des calculs auxquels nous ne nous intéressons pas ici. On notera qu'une seule ligne diffère entre la version à tableau statique et celle avec tableau dynamique.

Programmation avec un tableau ordinaire (statique) :

Sélectionnez

void Blaise(int N) {
    int n, p, i;
    int tab[N_MAX + 1]; /* N_MAX est une constante connue à la compilation */

    for (n = 0; n <= N; n++) {
        tab[0] = tab[n] = 1;
        for (p = n - 1; p > 0; p--)
            tab[p] += + tab[p - 1];
                /* exploitation de tab ; par exemple, affichage : */
        for (i = 0; i <= n; i++)
            printf("]", tab[i]);
        printf("\n");
    }
}

Programmation avec un tableau dynamique :

Sélectionnez

void Blaise(int N) {
        int n, p, i;
        int *tab; /* ici, pas besoin de constante connue à la compilation */

        tab = malloc((N + 1) * sizeof(int));
        if (tab == NULL)
            return;
        for (n = 0; n <= N; n++) {
            tab[0] = tab[n] = 1;
            for (p = n - 1; p > 0; p--)
                tab[p] += + tab[p - 1];
                /* exploitation de tab ; par exemple, affichage : */
        for (i = 0; i <= n; i++)
            printf("]", tab[i]);
        printf("\n");
    }
    free(tab); /* à ne pas oublier ( tab est une variable locale) */
}

Remarque. L'emploi de tableaux dynamiques à plusieurs indices est beaucoup plus compliqué que celui des tableaux à un indice comme ci-dessus, car il faut gérer soi-même les calculs d'indices, comme le montre la section suivante.

45Rappelons que les coefficients de la neme ligne du triangle de Pascal sont définis récursivement à partir de ceux de la n-1eme ligne par : Cpn = Cp-1n-1 + Cpn-1

VI-B-3. Tableaux multidimensionnels

Réflexions sur la double indexation. Supposons que nous ayons à écrire un programme qui opère sur des matrices à NL lignes et NC colonnes. La déclaration d'une telle matrice est facile à construire (cf. section V.D.1) :

Sélectionnez

tableau de NL tableau de NC float 		m1
tableau de NC float 				(m1)[NL] ou m1[NL]
float 						(m1[NL])[NC] ou m1[NL][NC]

Un élément particulier de cette matrice sera noté m1[i][j]. D'après ce que nous savons déjà, on peut l'interpréter de la manière suivante 46 :

Sélectionnez

m1[i][j]
= *(m1[i] + j)
= *(float *)((unsigned int) m1[i] + j * sizeof(float))

Le problème est : avec la déclaration que nous avons donnée, que signifie m1[i] ? Il est intéressant de constater que cette expression n'est pas une lvalue, puisqu'elle est de type « tableau de... », mais que, à part cela, elle est traitée comme un élément d'un tableau ordinaire. Autrement dit, si on supposait avoir la déclaration

Sélectionnez

typedef float LIGNE[NC]; /* LIGNE = tableau de NC float */

alors m1[i] serait une expression de type LIGNE valant :

Sélectionnez

m1[i]
= *(m1 + i)
= *(LIGNE *)((unsigned) m1 + i * sizeof(LIGNE))
= *(LIGNE *)((unsigned) m1 + i * NC * sizeof(float))

En oubliant pour un instant les types des pointeurs, il nous reste

Sélectionnez

m1[i][j] = *(m1 + (i * NC + j) * sizeof(float))

Nous retrouvons l'expression classique i * NC + j caractérisant l'accès à un élément mi,j d'une matrice NL*NC. Cette expression traduit la disposition « par lignes » des tableaux rectangulaires, comme le montre la figure 10.

Fig. 10 - Tableau bidimensionnel statique
Fig. 10 - Tableau bidimensionnel statique

Etudions une autre manière d'accéder aux éléments de cette même matrice. Soient les déclarations

Sélectionnez

float m1[NL][NC], *m2[NL];

La deuxième se lit, compte tenu des priorités : float *(m2[NL]). La variable m2 est donc déclarée comme un tableau de NL pointeurs vers des nombres flottants. Pour réaliser la matrice dont nous avons besoin, il nous suffit d'initialiser correctement ces pointeurs : chacun sera l'adresse d'une ligne de la matrice précédente. Nous aurons la configuration de la figure 11.

Fig. 11 - Tableau bidimensionnel réalisé avec un tableau de pointeurs
Fig. 11 - Tableau bidimensionnel réalisé avec un tableau de pointeurs

Il est remarquable qu'un élément de la nouvelle « matrice » ainsi déclarée se note encore

Sélectionnez

m2[i][j]

mais maintenant, cette expression se traduira (en continuant à négliger les conversions des types pointeurs) par :

Sélectionnez

m2[i][j]
    = *(m2[i] + j * sizeof(float))
    = *(*(m2 + i * sizeof(float *)) + j * sizeof(float))

Les multiplications par sizeof(float) et sizeof(float *) sont réalisées de manière très rapide par les calculateurs (ce sont des multiplications par des puissances de 2), nous pouvons les ignorer. Il reste que nous avons remplacé « m1 + i * NC + j » par « *(m2 + i) + j ». Nous avons donc bien éliminé une « vraie » multiplication : notre deuxième manière est plus efficace. En contrepartie nous occupons un peu plus d'espace (les NL pointeurs supplémentaires).

De plus, il nous faut initialiser le tableau de pointeurs m2. La matrice m1 existant par ailleurs, cela se fait ici par une expression d'une étonnante simplicité :

Sélectionnez

for (i = 0; i < NL; i++)
m2[i] = m1[i];

46Pour alléger les formules, nous supposons ici que la taille d'un int, et donc celle d'un unsigned, sont égales à celle d'un pointeur.

VI-B-4. Tableaux multidimensionnels dynamiques

Les considérations précédentes montrent pourquoi dans la réalisation d'une matrice dynamique on devra renoncer au mécanisme de double indexation ordinaire. En effet, NC doit être connu à la compilation pour que l'expression i * NC + j ait un sens.

Or nous sommes maintenant parfaitement équipés pour réaliser une telle matrice. Supposons que la fonction void unCalcul(int nl, int nc) implémente un algorithme qui requiert une matrice locale à nl lignes et nc colonnes.

Version statique :

Sélectionnez

#define NL 20
#define NC 30
...
void unCalcul(int nl, int nc) {
    double mat[NL][NC]; /* en espérant que nl · NL et nc · NC */
    int i, j;
        /* utilisation de la matrice mat. Par exemple : */
for (i = 0; i < nl; i++)
    for (j = 0; j < nc; j++)
        mat[i][j] = 0;
etc.
}

Version dynamique (par souci de clarté nous y avons omis la détection des échecs de malloc) :

Sélectionnez

void unCalcul(int nl, int nc) {
 double **mat;
 int i, j;
    /* initialisation des pointeurs */
 mat = malloc(nl * sizeof(double *));
 for (i = 0; i < nl; i++)
 mat[i] = malloc(nc * sizeof(double));
    /* utilisation de la matrice mat. Par exemple : */
 for (i = 0; i < nl; i++)
 for (j = 0; j < nc; j++)
 mat[i][j] = 0;
 etc.
    /* libération (indispensable dans le cas d'une variable locale) */
 for (i = 0; i < nl; i++)
 free(mat[i]);
 free(mat);
}

Dans cette manière de procéder, les lignes de la matrice sont allouées à l'occasion de nl appels distincts de malloc. La matrice est réalisée par des morceaux de mémoire éparpillée, comme le montre la figure 12.

Fig. 12 - Matrice dynamique (lignes allouées séparément)
Fig. 12 - Matrice dynamique (lignes allouées séparément)

Variante : lignes contiguës. Une légère variante consiste à allouer d'un seul coup toute la mémoire nécessaire aux lignes de la matrice. La structure de la matrice ressemble alors à celle de la figure 11 :

Sélectionnez

int unCalcul(int nl, int nc) {
    double **mat, *espace;
    int i, j;
        /* initialisation des pointeurs */
    mat = malloc(nl * sizeof(double *));
if (mat == NULL)
    return 0; /* échec */
        espace = malloc(nl * nc * sizeof(double));
if (espace == NULL) {
    free(mat);
        return 0; /* échec */
    }
for (i = 0; i < nl; i++)
mat[i] = espace + i * nc;
        /* utilisation de la matrice mat. Par exemple : */
for (i = 0; i < nl; i++)
for (j = 0; j < nc; j++)
mat[i][j] = 0;
etc.
        /* libération (indispensable dans le cas d'une variable locale) */
    free(espace);
    free(mat);
    return 1; /* succès */
}
Fig. 13 - Matrice de caractères
Fig. 13 - Matrice de caractères

VI-B-5. Tableaux de chaines de caractères

Le remplacement d'un tableau à deux indices par un tableau de pointeurs se révèle encore plus utile dans le traitement des tableaux de chaines de caractères. Ici, cette technique entraine une économie de place substantielle, puisqu'elle permet de remplacer un tableau rectangulaire dont la largeur est déterminée par la plus grande longueur possible d'une chaine (avec un espace inoccupé très important) comme celui de la figure 13, par un espace linéaire dans lequel chaque chaine n'occupe que la place qui lui est nécessaire, comme le montre la figure 14.

Fig. 14 - Tableau de chaines de caractères
Fig. 14 - Tableau de chaines de caractères

Nous allons illustrer cette méthodologie par un exemple très simple mais tout à fait typique. Notre programme lit des lignes de caractères au terminal et les range dans une table, jusqu'à la rencontre de la fin de fichier ou la lecture d'une ligne vide. Ensuite, il ordonne les lignes lues ; finalement il affiche les lignes ordonnées. Les lignes lues sont rangées les unes à la suite des autres dans un tableau unique, nommé espace, dont le pointeur libre indique la première place disponible. La table des mots, notée mots, est une table de renvois dans espace. La figure 14 illustre ces structures de données. Notez que le tri se fait en ordonnant la table des pointeurs ; les caractères eux-mêmes ne sont nullement déplacés.

Les fonctions gets (lecture de chaine), puts (écriture de chaine), strcmp (comparaison de chaines) et strlen (longueur d'une chaine) appartiennent à la bibliothèque standard et sont expliquées dans les sections suivantes.

Sélectionnez

#include <stdio.h>
#include <string.h>

#define MAXMOTS 100
#define MAXCARS 3000

char espace[MAXCARS];
char *libre = espace;
char *mots[MAXMOTS];
int nbr_mots = 0;

void tri(char *t[], int n) {
    for (;;) {
        int i, ok = 1;
        for (i = 1; i < n; i++)
            if (strcmp(t[i - 1], t[i]) > 0) {
                char *w = t[i - 1];
                t[i - 1] = t[i];
                t[i] = w;
                ok = 0;
        }
    if (ok)
    return;
    n--;
  }
}

void main(void) {
    int i;
    while(gets(libre) != NULL && *libre != '\0') {
        mots[nbr_mots++] = libre;
        libre += strlen(libre) + 1;
    }
    tri(mots, nbr_mots);
    for (i = 0; i < nbr_mots; i++)
        printf("%s\n", mots[i]);
}

Remarque. On peut signaler qu'une structure tout à fait analogue à notre table mots est créée par le compilateur lorsqu'on initialise un tableau de chaines de caractères, comme dans :

Sélectionnez

char *messages[] = {
    "Fichier inexistant",
    "Volume ou repertoire inexistant",
    "Nom de fichier incorrect",
    "Erreur physique d'entree-sortie",
    "Autre erreur" };

La seule différence entre notre tableau mots et le tableau messages que créerait le compilateur par suite de la déclaration ci-dessus est la suivante : les éléments de mots sont des pointeurs vers un espace modifiable faisant partie de la mémoire attribuée pour les variables de notre programme, tandis que messages est fait de pointeurs vers des cellules de la « zone de constantes », espace non modifiable alloué et garni une fois pour toutes par le compilateur.

VI-B-6. Tableaux multidimensionnels formels

Nous avons vu que si un argument formel d'une fonction ou une variable externe est un tableau à un seul indice, alors l'indication du nombre de ses éléments est sans utilité puisqu'il n'y a pas d'allocation d'espace. Mais que se passe-t-il pour les tableaux à plusieurs indices ? Si un tableau a été déclaré
T table[N1][N2] ... [Nk-1][Nk] ;
(N1, N2, etc. sont des constantes) alors un accès comme

Sélectionnez

table[i1][i2] ... [ik-1][ik]

se traduit par *( (T*)table + i1 * N2 * N3 * ... * Nk + ... + ik-1 * Nk + ik )
Qu'il y ait ou non allocation d'espace, il faut que ce calcul puisse être effectué. D'où la règle générale : lorsqu'un argument formel d'une fonction est un tableau à plusieurs indices, la connaissance de la première dimension déclarée est sans utilité mais les autres dimensions déclarées doivent être connues du compilateur. Pour un tableau à deux indices, cela donne : il est inutile d'indiquer combien il y a de lignes, mais il faut absolument que, en compilant la fonction, le compilateur connaisse la taille déclarée pour chaque ligne. Par exemple, la fonction suivante effectue la multiplication par un scalaire k de la matrice m déclarée NL * NC (deux constantes connues dans ce fichier) :

Sélectionnez

void k_fois(double k, double m[][NC]) {
    int i, j;
    for (i = 0; i < NL; i++)
        for (j = 0; j < NC; j++)
            m[i][j] *= k;
}

Remarque. Bien entendu, une autre solution aurait consisté à faire soi-même les calculs d'indice :

Sélectionnez

void k_fois(double k, void *m) {
int i, j;
for (i = 0; i < NL; i++)
for (j = 0; j < NC; j++)
*((double *) m + i * NC + j) *= k;
}

Tout ce qu'on y gagne est un programme bien plus difficile à lire !

VI-B-7. Tableaux non nécessairement indexés à partir de zéro

En principe, les tableaux de C sont indexés à partir de zéro : sans qu'il soit nécessaire de le spécifier, un tableau T déclaré de taille N est fait des éléments T0, T1 ... TN-1. Nous avons vu que, dans la grande majorité des applications, cette convention est commode et fiable.

Il existe cependant des situations dans lesquelles on souhaiterait pouvoir noter les éléments d'un tableau T1, T2 ... TN ou, plus généralement, Ta, Ta+1 ... Tb, a et b étant deux entiers quelconques vérifiant a · b. Nous allons voir que l'allocation dynamique fournit un moyen simple et pratique 47 pour utiliser de tels tableaux. Cas des tableaux à un indice. La fonction dTable48 crée un tableau d'éléments de type double dont les indices sont les entiers iMin, iMin+1, ... iMax :

Sélectionnez

double *dTable(int iMin, int iMax) {
double *res = malloc((iMax - iMin + 1) * sizeof(double));
if (res == NULL) {
erreurTable = 1;
return NULL;
}
erreurTable = 0;
return res - iMin;
}

(Si on avait besoin de tableaux de float, de int, etc. on écrirait des fonctions analogues fTable, iTable, etc.). La variable globale erreurTable est rendue nécessaire par le fait qu'un résultat NULL ne suffit pas ici à caractériser l'échec de l'allocation ; en effet, res - iMin peut être nul alors que l'allocation a réussi et res ≠ NULL. La figure 15 illustre le résultat rendu par la fonction précédente lors d'un appel comme dTable(5, 10).

Fig. 15 - Adresse rendue par la fonction dTable
Fig. 15 - Adresse rendue par la fonction dTable

0n accède aux éléments d'un tel tableau à l'aide d'un indice compris entre les valeurs indiquées mais, à part cela, de manière tout à fait habituelle ; en particulier, sans aucune perte d'efficacité. Exemple d'utilisation :

Sélectionnez

...
double *t = dTable(5, 10);
...
for (i = 5; i <= 10; i++)
t[i] = 1000 + i;
...
for (i = 5; i <= 10; i++)
printf("%.1f\n", t[i]);
...

Attention. Un tableau créé par un appel de la fonction dTable est dynamiquement alloué, cependant sa libération est un peu plus complexe que pour les tableaux dynamiques ordinaires. En particulier, à la suite du programme ci-dessus, un appel comme

Sélectionnez

free(t);

générera certainement une erreur à l'exécution. Il aurait fallu définir une structure plus complexe qu'un simple tableau, incluant soit la valeur de iMin, soit celle de res (actuellement des variables locales de la fonction dTable) pour gérer correctement la libération ultérieure d'un tel tableau.

Cas des tableaux à plusieurs indices. Les remarques précédentes, avec celles faites à la section 6.2.4, suggèrent une manière de mettre en œuvre des matrices dont les lignes et les colonnes ne sont pas indexées à partir de zéro.

La fonction dMatrice crée une matrice dont les lignes sont indexées par les entiers lMin, lMin+1 ... lMax et les colonnes sont indexées par les entiers cMin, cMin+1 ... cMax :

Sélectionnez

double **dMatrice(int lMin, int lMax, int cMin, int cMax) {
    int nbLin, nbCol, i;
    double *espace, **lignes, *p;

    nbLin = lMax - lMin + 1;
    nbCol = cMax - cMin + 1;
    espace = malloc(nbLin * nbCol * sizeof(double));
    if (espace == NULL) {
        erreurTable = 1;
        return NULL;
    }
    lignes = malloc(nbLin * sizeof(double *));
    if (lignes == NULL) {
        erreurTable = 1;
        free(espace);
        return NULL;
    }
    p = espace - cMin;
    for (i = 0; i < nbLin; i++) {
        lignes[i] = p;
        p += nbCol;
    }
    erreurTable = 0;
    return lignes - lMin;
}

Comme précédemment, l'emploi de ces matrices ne s'accompagne d'aucune perte d'efficacité. Voici un exemple d'utilisation :

Sélectionnez

...
double **m = dMatrice(5, 10, -3, 3);
...
for (i = 5; i <= 10; i++)
    for (j = -3; j <= 3; j++)
        m[i][j] = 1000 * i + j;
...
for (i = 5; i <= 10; i++) {
    for (j = -3; j <= 3; j++)
        printf("%10.1f ", m[i][j]);
    printf("\n");
}
...

47Nous expliquons ici une technique massivement employée dans la bibliothèque de calcul scientifique Numerical Recipes (°c Numerical Recipes Software) basée sur le livre Numerical Recipes in C (W. Press, S. Teukolsky, W. Vetterling et B. Flannery, Cambridge University Press, 1992).

48D'obscures contraintes techniques, ayant perdu tout intérêt de nos jours, font que cette technique peut ne pas fonctionner dans les implantations de C sur d'anciennes plate-formes, comme les PC à base de processeurs Intel antérieurs au 80286 (un lointain ancêtre du Pentium), dans lesquelles l'arithmétique des adresses subit des limitations drastiques.

VI-B-8. Matrices non dynamiques de taille inconnue

Un dernier cas qui se présente parfois est celui ou on doit écrire une fonction opérant sur des matrices ordinaires (c'est-à-dire non dynamiques) alors que leurs dimensions ne sont pas connues lors de la compilation. Par exemple, voici la fonction qui effectue le produit de deux matrices A et B en déposant le résultat dans une troisième matrice C. Les arguments nla et nca représentent le nombre effectif de lignes et de colonnes de A, tandis que ncb est le nombre effectif de colonnes de B. Les arguments dNca, dNcb et dNcc représentent les nombres de colonnes avec lesquels on a déclaré les matrices A, B et C respectivement.

Sélectionnez

void produitMatrices(double *A, double *B, double *C,
        int nla, int nca, int ncb, int dNca, int dNcb, int dNcc) {
    int i, j, k;
    double s;

    for (i = 0; i < nla; i++)
        for (j = 0; j < ncb; j++) {
            s = 0;
            for (k = 0; k < nca; k++) /*1*/
                s += A[i * dNca + k] * B[k * dNcb + j]; /*1*/
            C[i * dNcc + j] = s;
    }
}

Exemple d'appel (NLA, NCA, etc., sont des constantes introduites par des #define, tandis que nla, nca, etc. sont des variables) :

Sélectionnez

double A[NLA][NCA], B[NLB][NCB], C[NLC][NCC];
...
obtention des valeurs des variables nla, nca, etc., et des matrices A et B
...
produitMatrices(&A[0][0], &B[0][0], &C[0][0], nla, nca, ncb, NCA, NCB, NCC);
...

On notera que la fonction produitMatrices peut être optimisée en supprimant des des multiplications qui sont faites dans la boucle la plus profonde (la seule qui doit nous intéresser, du point de vue de l'efficacité). Pour cela, il suffit de remplacer les deux lignes marquées /*1*/ par ceci (ia et ib sont des variables locales nouvelles) :

Sélectionnez

        ...
ia = i * dNca; 					/*2*/
ib = j; 						/*2*/
for (k = 0; k < nca; k++) { 	/*2*/
    s += A[ia] * B[ib]; 		/*2*/
    ia += 1; 					/*2*/
    ib += dNcb; 				/*2*/
} /*2*/
...

VI-C. Les adresses des fonctions

VI-C-1. Les fonctions et leurs adresses

En étudiant les déclarateurs complexes (section 5.4) nous avons appris que la syntaxe de C permettait de manipuler le type « fonction rendant un [objet de type] T » (T représente un autre type), cette périphrase constituant elle-même un déclarateur que le programmeur peut composer librement avec les autres déclarateurs, « adresse d'un [objet de type] T » et « tableau de [objets de type] T ». Tout cela suggère qu'en C on aura le droit de travailler avec des variables qui représentent des fonctions, des tableaux de fonctions, des fonctions renvoyant comme résultat d'autres fonctions, etc. Qu'en est-il exactement ?

Le concept le plus simple qu'on peut souhaiter dans ce domaine est celui de variable dont la valeur est une fonction. Il va falloir y renoncer, pour une raison facile à comprendre : la taille d'une fonction ne peut pas se déduire de son type49. C'est un inconvénient rédhibitoire, car toute définition de variable doit entrainer la réservation d'un espace mémoire pour en contenir la valeur, ce qui demande de connaitre combien d'octets sont nécessaires.

En revanche, le langage C permet d'utiliser un concept à peine plus complexe que le précédent : celui de variable dont la valeur est l'adresse d'une fonction. Il n'y a plus de problème de taille (la taille d'une adresse - ou pointeur - est constante), et la syntaxe pour déclarer une telle variable a déjà été expliquée dans la section sur les déclarateurs complexes (cf. section V.D). Exemple :

Sélectionnez

double (*uneFonction)(int n, double x);

Cet énoncé déclare uneFonction comme une variable (un pointeur) destinée à contenir des adresses de fonctions ayant deux arguments, un int et un double, et rendant une valeur double. Cette déclaration n'initialise pas uneFonction (sauf, si c'est une variable globale, avec la valeur 0, peu utile ici) et, à part le type des arguments et du résultat, ne laisse rien deviner sur les caractéristiques des fonctions dont uneFonction contiendra les adresses.

Si, par la suite, uneFonction reçoit l'adresse d'une fonction Á pour valeur, alors l'expression suivante est correcte et représente un appel de φ (en supposant k de type int et u et v de type double) :

Sélectionnez

u = (*uneFonction)(k, v);

Comment le programmeur obtient-il des adresses de fonctions ? Il faut savoir que, de la même manière que le nom d'un tableau représente son adresse de base, le nom d'une fonction représente l'adresse de celle-ci. Ainsi, si on a la définition de fonction

Sélectionnez

double maFonction(int nbr, double val) {
/* etc. */
}

alors l'expression maFonction :

  • possède le type « adresse d'une fonction ayant pour arguments un int et un double et rendant un double » ;
  • a pour valeur l'adresse ou commence la fonction maFonction ;
  • n'est pas une lvalue.

Par conséquent, l'affectation suivante est tout à fait légitime :

Sélectionnez

uneFonction = maFonction;

A partir de cette affectation (et tant qu'on aura pas changé la valeur de uneFonction) les appels de (*uneFonction) seront en fait des appels de maFonction.

Examinons deux applications de cela parmi les plus importantes.

49Notez que, malgré les apparences, il n'est pas délirant de rapprocher les fonctions et les données. Une fois compilée (traduite en langage machine), une fonction est une suite d'octets qui occupe une portion de mémoire exactement comme, par exemple, un tableau de nombres. La principale différence est dans la possibilité ou l'impossibilité de prédire le nombre d'octets occupés.

Pour un tableau, par exemple, ce nombre découle sans ambigüité de la déclaration. Pour une fonction il ne dépend pas de la déclaration (qui ne précise que le type du résultat et le nombre et les types des arguments) mais du nombre d'instructions qui composent la fonction, c'est-à-dire de ce qu'elle fait et comment elle le fait.

VI-C-2. Fonctions formelles

Le problème des fonctions formelles, c'est-à-dire des fonctions arguments d'autres fonctions, est résolu en C par l'utilisation d'arguments de type « pointeur vers une fonction ».

Exemple 1 : résolution de f(x) = 0 par dichotomie. Si f est une fonction continue, définie sur un intervalle [ a ... b ] telle que f(a) et f(b) ne sont pas de même signe, la fonction dicho recherche xε ∈ [ a ... b ] telle qu'il existe x0 ∈ [ a ... b ] vérifiant |xε - x0| < ε et f(x0) = 0. En clair : dicho détermine xε, qui est une solution de l'équation f(x) = 0 si on tolère une erreur d'au plus ε.

Les arguments de la fonction dicho sont : les bornes a et b de l'intervalle de recherche, la précision " voulue et surtout la fonction f dont on cherche un zéro. Voici cette fonction :

Sélectionnez

double dicho(double a, double b, double eps, double (*f)(double)) {
    double x; 			/* hypothèse : f(a) * f(b) <= 0 */
    if ((*f)(a) > 0) {
        x = a; a = b; b = x;
    }
    while (fabs(b - a) > eps) {
        x = (a + b) / 2;
        if ((*f)(x) < 0)
            a = x;
        else
            b = x;
    }
    return x;
}

L'argument f est déclaré comme un pointeur vers une fonction ; par conséquent *f est une fonction et (*f)(x) un appel de cette fonction. Si une fonction est le nom d'une fonction précédemment définie, changeant de signe sur l'intervalle [0, 1], un appel correct de dicho serait

Sélectionnez

x = dicho(0.0, 1.0, 1E-8, une_fonction);

Autre exemple du même tonneau

Sélectionnez

y = dicho(0.0, 3.0, 1E-10, cos);

Remarque. En syntaxe originale, ou « sans prototype », la fonction dicho s'écrirait

Sélectionnez

double dicho(a, b, eps, f)
double a, b, eps, (*f)();
{
etc. (la suite est la même)
}

Exemple 2 : utilisation de qsort. Reprenons notre programme de la section 6.2.5 (tri d'un tableau de chaines de caractères) en remplaçant la fonction de tri naïf par la fonction qsort de la bibliothèque standard. Cette fonction est expliquée à la section 8.4.2, elle trie un tableau par rapport à un critère quelconque. Si elle nous intéresse ici c'est parce qu'elle prend ce critère, représenté par une fonction, pour argument :

Sélectionnez

#include <stdio.h>
#include <string.h>

#define MAXMOTS 100
#define MAXCARS 3000

char espace[MAXCARS], *libre = espace;
char *mots[MAXMOTS];
int nbr_mots = 0;
    /* La déclaration suivante se trouve aussi dans <stdlib.h> : */
void qsort(void *base, size_t nmemb, size_t size,
int (*comp)(const void *, const void *));
int comparer(const void *a, const void *b) {
    /* Les arguments de cette fonction doivent être déclarés "void *" (voyez la */
    /* déclaration de qsort). En réalité, dans les appels de cette fonction que */
    /* nous faisons ici, a et b sont des adresses de "chaines", c'est-à-dire des */
    /* adresses d'adresses de char */
return strcmp(*(char **)a, *(char **)b);
}

main() {
    int i;
    while(gets(libre) != NULL && *libre != '\0') {
        mots[nbr_mots++] = libre;
        libre += strlen(libre) + 1;
    }
    qsort(mots, nbr_mots, sizeof(char *), comparer);
    for (i = 0; i < nbr_mots; i++)
        printf("%s\n", mots[i]);
}

Remarque. La définition de comparer peut paraitre bien compliquée. Pour la comprendre, il faut considérer les diverses contraintes auxquelles elle doit satisfaire :

  • comparer figurant comme argument dans l'appel de qsort, son prototype doit correspondre exactement 50 à celui indiqué dans le prototype de qsort ;
  • a et b, les arguments formels de comparer, étant de type void *, on ne peut accéder aux objets qu'ils pointent sans leur donner auparavant (à l'aide de l'opérateur de conversion de type) un type pointeur « utilisable » ;
  • d'après la documentation de qsort, nous savons que comparer sera appelée avec deux adresses de chaines pour arguments effectifs ; une chaine étant de type char *, le type « adresse de chaine » est char **, ce qui explique la fa»con dont a et b apparaissent dans l'appel de strcmp.

50En syntaxe « sans prototype », aucun contrôle n'est fait sur le type des arguments de comparer, et toute cette question est bien plus simple. Mais attention : quelle que soit la syntaxe employée, on ne peut pas ignorer le fait que comparer sera appelée (par qsort) avec pour arguments effectifs les adresses des chaines à comparer.

VI-C-3. Tableaux de fonctions

Le programme suivant montre l'utilisation d'un tableau d'adresses de fonctions : chaque élément du tableau table est formé de deux champs :

  • le nom d'une fonction standard, sous forme de chaine de caractères ;
  • l'adresse de la fonction correspondante.

Notre programme lit des lignes constituées d'un nom de fonction, suivi d'un ou plusieurs blancs, suivis d'un nombre réel ; il évalue et affiche alors la valeur de la fonction mentionnée appliquée au nombre donné. La frappe d'une ligne réduite au texte fin arrête le programme.

Sélectionnez

#include <stdio.h>

double sin(double), cos(double), exp(double), log(double);

struct {
    char *nom;
    double (*fon)(double);
} table[] = {
    "sin", sin,
    "cos", cos,
    "exp", exp,
    "log", log };

#define NBF (sizeof table / sizeof table[0])

main() {
    char nom[80];
    double x;
    int i;
    for(;;) {
        scanf("%s", nom);
        if (strcmp(nom, "fin") == 0)
            break;
    scanf("%lf", &x);
    for(i = 0; i < NBF && strcmp(table[i].nom, nom) != 0; i++)
        ;
    if (i < NBF)
        printf("%f\n", (*table[i].fon)(x));
    else
        printf("%s ???\n", nom);
    }
}

Voici une exécution de ce programme :

Sélectionnez

sin 1
0.841471
cos 1
0.540302
atn 1
atn ???
fin

Remarque. La déclaration du tableau table présente une caractéristique peu fréquente : des fonctions sont référencées mais elles ne sont pas en même temps appelées. En effet, les noms des fonctions sin, cos, exp, etc. apparaissent dans cette déclaration sans être accompagnés d'une paire de parenthèses ; il n'y a aucun signe indiquant au compilateur qu'il s'agit de fonctions. C'est pourquoi ce programme sera trouvé erroné par le compilateur à moins qu'on lui donne les déclarations externes des fonctions, soit explicitement : double sin(double), cos(double), exp(double), log(double);

soit en incluant un fichier dans lequel sont écrites ces déclarations :

Sélectionnez

#include <math.h>

VI-C-4. Flou artistique

A ce point de notre exposé vous êtes en droit de trouver que les rôles respectifs des objets de type « fonction... » et des objets de type « adresse d'une fonction... » sont confus. Examinons une fonction bien ordinaire, sinus. La déclaration de cette fonction, extraite du fichier <math.h>, est :

Sélectionnez

double sin(double);

D'autre part, la définition d'une variable de type « adresse d'une fonction double à un argument double » est : double (*f)(double);

Voici l'initialisation de f par l'adresse de la fonction sin et deux appels équivalents de cette fonction (x, y et z sont des variables double) :

Sélectionnez

f = sin;
y = (*f)(x);
z = sin(x);

Quelle que soit la valeur de x, y et z reçoivent la même valeur. Pourtant, les trois lignes ci-dessus ne sont pas cohérentes. La première montre que f et sin sont de même type (à savoir « adresse d'une fonction... ») ; or f doit être déréférencé pour appeler la fonction, tandis que sin n'a pas besoin de l'être.

On peut comprendre la raison de cette incohérence. Après avoir constaté l'impossibilité de déclarer des variables de type « fonction... » , car une fonction n'a pas de taille définie, et donc l'obligation de passer par des variables « adresse de fonction... », on a voulu faire cohabiter la rigueur (un pointeur doit être déréférencé pour accéder à l'objet qu'il pointe) et la tradition (le sinus de x s'est toujours écrit sin(x)). Cela a conduit à définir l'opérateur d'appel de fonction aussi bien pour une expression de type « fonction... » que pour une expression de type « adresse d'une fonction... ». Ainsi, les deux expressions (*f)(x) et sin(x) sont correctes ; l'expérience montre que cette tolérance n'a pas de conséquences néfastes. Il n'en découle pas moins que les deux expressions

Sélectionnez

u = f(x);
v = (*sin)(x);

sont acceptées elles aussi (elles ont la même valeur que les précédentes).

VI-D. Structures récursives

VI-D-1. Déclaration

Les structures récursives font référence à elles-mêmes. Elles sont principalement utilisées pour implanter des structures de données dont la définition formelle est un énoncé récursif. Parmi les plus fréquemment rencontrées : les listes et les arbres, comme les arbres binaires.

Ainsi, par exemple, on peut donner la définition récursive suivante : un arbre binaire est

  • soit un symbole conventionnel signifiant « la structure vide » ;
  • soit un triplet ( valeur , filsg , filsd ) constitué d'une information dont la nature dépend du problème étudié et de deux descendants qui sont des arbres binaires.

Bien entendu, du point de vue technique il n'est pas possible de déclarer un champ d'une structure comme ayant le même type que la structure elle-même (une telle structure serait ipso facto de taille infinie !). On contourne cette difficulté en utilisant les pointeurs. Un arbre binaire est représenté par une adresse, qui est :

  • soit l'adresse nulle ;
  • soit l'adresse d'une structure constituée de trois champs : une information et deux descendants qui sont des adresses d'arbres binaires.

Typiquement, la déclaration d'une telle structure sera donc calquée sur la suivante :

Sélectionnez

struct noeud {
type valeur;
struct noeud *fils_gauche, *fils_droit;
};

Notez que le nom de la structure (noeud) est ici indispensable, pour indiquer le type des objets pointés par les champs fils gauche et fils droit.

VI-D-2. Exemple

Le programme suivant effectue le même travail que celui du 6.2.5, mais à la place d'une table on utilise un arbre binaire de recherche (voir, par exemple, R. Sedgewick, Algorithmes en langage C, chapitre 14) pour ranger les mots. La structure de données mise en œuvre est représentée sur la figure 16.

Sélectionnez

#include <stdlib.h>
#include <stdio.h>

#define MAXCARS 3000 char

espace[MAXCARS], *libre = espace;

typedef struct noeud {
    char *mot;
    struct noeud *fils_gauche, *fils_droit;
} NOEUD, *POINTEUR;

POINTEUR alloc(char *m, POINTEUR g, POINTEUR d) {
    POINTEUR p = malloc(sizeof(NOEUD));
    p->mot = m;
    p->fils_gauche = g;
    p->fils_droit = d;
    return p;
}

POINTEUR insertion(POINTEUR r, char *mot) {
    if (r == NULL)
        r = alloc(mot, NULL, NULL);
    else
        if (strcmp(mot, r->mot) <= 0)
            r->fils_gauche = insertion(r->fils_gauche, mot);
    else
            r->fils_droit = insertion(r->fils_droit, mot);
 return r;
}
Fig. 16 - Arbre binaire de mots
Fig. 16 - Arbre binaire de mots
Sélectionnez

void parcours(POINTEUR r) {
if (r != NULL) {
parcours(r->fils_gauche);
printf("%s\n", r->mot);
parcours(r->fils_droit);
}
}
main() {
POINTEUR racine = NULL;
char *p;
p = gets(libre);
while(*p != '\0') {
libre += strlen(p) + 1;
racine = insertion(racine, p);
p = gets(libre);
}
parcours(racine);
}

VI-D-3. Structures mutuellement récursives

Donnons-nous le problème suivant : représenter une « liste de familles » ; chaque famille est une « liste d'individus », avec la particularité que chaque individu fait référence à sa famille. Les structures famille et individu se pointent mutuellement, chacune intervenant dans la définition de l'autre. Comment les déclarer ?

Fig. 17 - Structures mutuellement récursives
Fig. 17 - Structures mutuellement récursives

Pour résoudre ce problème le compilateur C tolère qu'on déclare un pointeur vers une structure non encore définie. Plus précisément, si la structure ayant le nom indiqué n'a pas encore été déclarée, l'expression « struct nom » est légitime. Elle identifie un type incomplet et on peut l'utiliser à tout endroit ou sa taille n'est pas requise, notamment dans la déclaration d'un pointeur vers une telle structure. Cela nous permet de déclarer nos structures mutuellement récursives sous la forme :

Sélectionnez

typedef struct famille {
    char *nom;
    struct individu *membres;
    struct famille *famille_suivante;
} FAMILLE;

typedef struct individu {
    char *prenom;
    struct individu *membre_suivant;
    struct famille *famille;
} INDIVIDU;

Une autre solution aurait été :

Sélectionnez

typedef struct famille FAMILLE;
typedef struct individu INDIVIDU;

struct famille {
    char *nom;
    INDIVIDU *membres;
    FAMILLE *famille_suivante;
};

struct individu {
    char *prenom;
    INDIVIDU *membre_suivant;
    FAMILLE *famille;
};


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • VII. Entrées-sorties
    • VII-A. Flots
      • VII-A-1. Fonctions générales sur les flots
      • VII-A-2. Les unités standard d'entrée-sortie
    • VII-B. Lecture et écriture textuelles
      • VII-B-1. Lecture et écriture de caractères et de chaines
      • VII-B-2. Ecriture avec format printf
      • VII-B-3. Lecture avec format scanf
      • VII-B-4. A propos de la fonction scanf et des lectures interactives
      • VII-B-5. Les variantes de printf et scanf
    • VII-C. Opérations en mode binaire
      • VII-C-1. Lecture-écriture
      • VII-C-2. Positionnement dans les fichiers
    • VII-D. Exemples
      • VII-D-1. Fichiers "en vrac"
      • VII-D-2. Fichiers binaires et fichiers de texte
      • VII-D-3. Fichiers en accès relatif
    • VII-E. Les fichiers de bas niveau d'UNIX

VII. Entrées-sorties

Strictement parlant, les opérations d'entrée-sortie ne font pas partie du langage C; elles sont effectuées par des fonctions de la bibliothèque que chaque système offre avec le compilateur. Or, C est né et a vécu longtemps en milieu UNIX avant d'être transporté ailleurs ; la bibliothèque UNIX a donc souvent été prise pour référence, semant quelque confusion car la réalisation de certaines fonctions d'UNIX est inutile, voire impossible sur certains systèmes. La norme ANSI y a mis bon ordre ; en principe la liste des fonctions standard est maintenant clairement établie et un programme qui n'utilise que ces fonctions doit pouvoir être transporté d'un système à un autre sans modification. Rien n'empêche que telle ou telle réalisation particulière du langage propose des fonctions additionnelles ; il faudra alors vous référer à la documentation spécifique.

VII-A. Flots

Quelles que soient les particularités du système d'exploitation sous-jacent, les fonctions de la bibliothèque standard des entrées-sorties font en sorte que les fichiers soient vus par le programmeur comme des flots, c'est- à-dire des suites d'octets qui représentent des données selon une des deux modalités suivantes :

  • soit le fichier contient les caractères qui constituent l'expression écrite des données en question d'après une certaine syntaxe. Ces caractères sont eux-mêmes représentés selon une convention largement répandue, presque toujours le code ASCII. De tels fichiers sont appelés fichiers de texte. Leur principale qualité est d'être exploitables par un logiciel, un système ou un équipement différents de celui qui les a produits ; en particulier, ils peuvent être affichés, édités, imprimés, etc. ;
  • soit le fichier contient des données enregistrées sous une forme qui est la copie exacte de leur codage dans la mémoire de l'ordinateur. On l'appelle alors fichier binaire. Les opérations de lecture ou d'écriture sur de tels fichiers sont très rapides, car elles ne requièrent pas un travail d'analyse ou de synthèse de l'expression écrite des données. En revanche, les fichiers binaires ne sont pas éditables ou imprimables ; ils ne sont pas censés être transportables d'un logiciel à un autre, encore moins d'un système à un autre.

Du point de vue du langage C (il n'en est peut être pas de même pour le système sous-jacent) être de texte ou binaire n'est pas une propriété d'un fichier, mais de la manière dont il est traité par un programme. Ainsi la distinction entre ces deux types de fichiers ne se fait pas lors de la déclaration ou de l'ouverture 51 du fichier, mais par le choix des fonctions de lecture ou d'écriture qui sont utilisées : certaines fonctions (fgets et fputs, lecture et écriture d'une ligne, et fscanf et fprintf, lecture et écriture de données formatées) n'ont de sens que sur un flot de texte. D'autres fonctions (fread, fwrite) effectuent la lecture et l'écriture de paquets d'octets sans interprétation aucune, et correspondent donc plutôt aux flots binaires 52.

Les flots binaires sont de simples suites d'octets. Les flots de texte ont une structure légèrement plus riche, puisqu'ils sont censés être organisés comme des suites de lignes. Chaque ligne est faite d'un nombre quelconque de caractères distincts de '\n' et se termine par le caractère '\n'. Même si sur un système particulier les fichiers de texte ne sont pas ainsi organisés, ou si le caractère qui indique la fin d'une ligne est différent, la bibliothèque standard fera en sorte qu'ils puissent être vus de cette manière par le programmeur 53.

Tampons. Un fichier est dit tamponné 54 lorsque les transferts physiques d'octets entre le fichier et un programme qui le lit ou l'écrit se font par paquets de taille fixe, la taille du tampon, même si les échanges logiques sont faits par paquets de taille variable ou même octet par octet. Par défaut un flot est toujours associé à un tampon de taille prédéterminée, mais le programmeur peut, à l'aide de la fonction setvbuf (cf. section VII.A.1), modifier la taille de ce dernier ou même le supprimer complètement.

Ce mode de gestion des fichiers réduit le nombre d'opérations physiques d'entrée - sortie (beaucoup plus lentes, quelles que soient les performances des disques durs actuels, que les transferts d'information purement internes) mais introduit un décalage dans leur chronologie. Ce que le programmeur croit être une opération d'écriture dans un fichier n'est en réalité qu'une « opération logique », c'est-à-dire une recopie dans un tampon situé en mémoire, dont le contenu sera transféré ultérieurement vers le fichier. Par conséquent, il est difficile ou impossible de savoir dans quel état se trouve le fichier à un moment donné ; de plus, si le programme vient à subir une terminaison anormale, une partie des informations logiquement écrites dans le fichier seront perdues, parce qu'elles n'ont jamais été physiquement transférées 55.

51Il y a quand-même un point de détail qu'il fait fixer lors de l'ouverture (voyez la fonction fopen) : y a-t-il lieu de guetter les marques de fins-de-ligne pour les transformer dans le caractère 'nn' et réciproquement ?

52Conséquence : sauf si le système sous-jacent l'interdit, un fichier écrit comme un °ot de texte peut être lu comme un °ot binaire. Cela revient à ignorer le fait que les octets dont il se compose représentent des données d'un ordre supérieur. La démarche inverse, lire comme un °ot de texte un fichier binaire, n'a pas de signification et peut provoquer des erreurs fatales (les fins de ligne seront absentes ou incomprises, le tampon débordera, etc.).

53En particulier, le couple (CR,LF) utilisé par certains systèmes est traduit en lecture par l'unique caractère 'nn'. Inversement, sur de tels systèmes, l'écriture « logique » du caractère 'nn' se traduit par l'écriture effective du couple (CR,LF).

54En bon franglais, on dit plutôt « bufferisé ».

55S'il existe une probabilité non négligeable pour qu'un programme ait une fin anormale (par exemple à cause de conditions d'exploitation difficiles) il est prudent de programmer des appels réguliers de la fonction fflush (cf. section VII.A.1)

VII-A-1. Fonctions générales sur les flots

Les flots sont représentés dans les programmes par des variables de type FILE *. La structure FILE est définie dans le fichier <stdio.h>. Pour utiliser un ou plusieurs flots il faut donc écrire en tête de son programme la directive

Sélectionnez

#include <stdio.h>

puis une déclaration de la forme

Sélectionnez

FILE *fichier;

Un fichier est donc représenté en C par une variable d'un type pointeur. Par conséquent, sa déclaration n'autorise aucune des opérations propres aux fichiers, aussi longtemps qu'on n'aura pas fait le nécessaire (voyez la fonction fopen ci-après) pour qu'il pointe sur une structure légitimement allouée et qui constitue la description d'un fichier ouvert.

Parmi les principales fonctions qui effectuent les opérations générales (ouverture, fermeture, etc.) sur les flots nous avons :

Sélectionnez

FILE *fopen(const char *nom, const char *mode)

Cette fonction ouvre le fichier dont le nom est indiqué par la chaine nom et rend un pointeur sur le flot correspondant, ou NULL si l'opération a échoué (fichier absent, etc.). Le nom doit être correct pour le système d'exploitation sous-jacent ; cela ne regarde pas le langage C.

Les valeurs permises pour mode sont :

  • "r" (read) ouverture d'un fichier. Le fichier doit exister ; son contenu n'est pas détruit. Le descripteur du flot créé est positionné en lecture et au début du fichier. En principe, seules les opérations de lecture sont permises sur ce flot
  • "r+" comme "r", mais les opérations d'écriture sont permises aussi
  • "w" (write) création d'un fichier. Le fichier peut exister ou non ; s'il existe, son contenu est entièrement effacé. Le descripteur du flot créé est positionné en écriture et au début du fichier (qui est vide). En principe, seules les opérations d'écriture sont permises sur ce flot
  • "w+" comme "w", mais les opérations de lecture sont permises aussi
  • "a" (append) allongement d'un fichier. Le fichier existe ou non ; s'il existe, son contenu n'est pas effacé. Le descripteur du flot créé est positionné en écriture et à la fin du fichier. Seules les opérations d'écriture sont permises
  • "a+" comme "a", mais les opérations de lecture sont permises aussi
  • "rb", "r+b", "wb", "w+b", "ab", "a+b" : si on envisage d'utiliser le fichier en mode binaire il faut ajouter la lettre b au mode ("r+b", "w+b" et "a+b" peuvent se noter aussi "rb+", "wb+" et "ab+")

Remarque 1. La seule différence que la lettre b dans le mode introduit dans le fonctionnement de toutes les fonctions d'entrée-sortie est la suivante : si le fichier est « de texte » (i.e. si la lettre b ne figure pas dans le mode) alors :

  • en lecture, chaque occurrence d'une marque de fin de ligne est détectée et, si cette marque n'est pas le caractère '\n' lui-même, remplacée par l'unique caractère '\n',
  • toute demande d'écriture du caractère '\n' produit en fait l'envoi effectif au fichier de la marque de fin de ligne requise par le système sous-jacent.

Il n'est donc pas exclu que sur un système d'exploitation donné l'ajout de la lettre b au mode soit sans effet. C'est notamment le cas d'UNIX, ou les fins de ligne sont indiquées par le caractère '\n'.

Remarque 2. Sur l'intérêt qu'il peut y avoir à effectuer des lectures et des écritures sur le même fichier, voir les remarques faites à la section 7.4.3 à propos des fichiers en accès relatif.

Sélectionnez

int fflush(FILE *flot)

Dans le cas d'un flot de sortie, cette fonction provoque l'écriture « physique » immédiate du tampon en cours de remplissage. Elle rend EOF en cas d'erreur, zéro dans les autres cas.

D'après la norme officielle du langage C, l'effet de fflush sur un flot qui n'est pas un flot de sortie est indéfini. Mais pour la plupart des bibliothèques actuelles, l'appel de cette fonction sur un flot d'entrée supprime les caractères disponibles dans le tampon. Par exemple, dans le cas fréquent ou l'entrée standard correspond au clavier, l'appel fflush(stdin) fait disparaitre tous les caractères déjà tapés mais pas encore lus par le programme.

Remarque. Si le fichier physique qui correspond au flot indiqué est un organe interactif, par exemple l'écran d'un poste de travail, alors la fonction fflush est implicitement appelée dans deux circonstances très fréquentes :

  • l'écriture du caractère '\n' qui produit l'émission d'une marque de fin de ligne et la vidange effective du tampon,
  • le début d'une opération de lecture sur l'unité d'entrée associée (les organes d'entrée-sortie interactifs forment généralement des couples) ; ainsi, par exemple, une lecture au clavier provoque la vidange du tampon d'écriture à l'écran. Cela permet qu'une question soit effectivement affichée avant que l'utilisateur ne doive taper la réponse correspondante.
Sélectionnez

int fclose(FILE *flot)

Cette fonction ferme le flot indiqué, produisant l'écriture physique des tampons, la restitution de l'espace alloué pour le tampon et les descripteurs du flot, etc. Elle rend zéro en cas de succès, une autre valeur en cas d'erreur.

A la suite d'un appel comme fclose(flot) l'emploi de la variable flot est illégitime. Oublier de fermer, par un appel de fclose, un fichier qui a été ouvert en lecture n'est en général pas une erreur importante. Mais oublier de fermer un fichier qui a été ouvert en écriture (c'est-à-dire un fichier qui vient d'être créé) peut être fatal pour l'information qui y a été écrite, car

  • le contenu du tampon d'écriture en cours de formation lors de la terminaison du programme ne sera probablement pas sauvegardé dans le fichier ;
  • dans certains systèmes, les fichiers nouvellement créés ne subissent les opérations qui les rendent connus du système d'exploitation (comme l'inclusion de leur nom dans un répertoire) qu'au moment de leur fermeture. Un fichier qui n'a jamais été fermé a donc une existence très précaire.
Sélectionnez

int feof(FILE *flot)

Cette fonction n'a d'intérêt que si flot désigne un fichier en lecture. Elle renvoie une valeur non nulle si et seulement si l'indicateur de fin de fichier du flot indiqué est vrai. Cela se produit non pas lorsque le fichier est positionné sur sa fin mais lorsqu'il est placé au-delà de sa fin.

Autrement dit, feof(fic) devient vrai immédiatement après qu'une lecture sur fic ait échoué parce qu'il n'y avait plus [assez] d'informations dans fic pour la satisfaire.

En particulier, feof n'est jamais vrai après l'ouverture (en lecture) du fichier, même lorsque ce dernier est vide. Il faut au moins une [tentative de] lecture pour, éventuellement, le rendre vrai.

Cette spécification de la fonction feof fonde le schéma suivant, de lecture séquentielle d'un fichier :

Sélectionnez

FILE *fichier;
...
fichier = fopen(nom, "rb");
/* vérification du succès de l'ouverture de fichier */
...
/* lecture de données sur fichier */
while ( ! feof(fichier)) {
    traitement des données lues
    lecture de données sur fichier
}
...
int ferror(FILE *flot)

Cette fonction renvoie une valeur non nulle si et seulement si l'indicateur d'erreur du flot indiqué est « vrai ». Elle doit être appelée immédiatement après une entrée-sortie sur ce flot pour savoir si l'opération en question a échoué. La variable globale entière errno (déclarée dans <errno.h>) contient alors un code renseignant sur la nature de l'erreur.

Sélectionnez

FILE *tmpfile(void)

Cette fonction crée un fichier temporaire en mode "w+b", sans nom externe, qui sera automatiquement détruit à la terminaison du programme. Elle renvoie le flot associé ou NULL en cas d'erreur.

Sélectionnez

int setvbuf(FILE *flot, char *tampon, int mode, size t taille)

Cette fonction redéfinit le tampon associé à un flot ouvert. Elle doit être appelée après l'ouverture du flot mais avant toute lecture ou écriture. L'argument mode doit être une des trois constantes suivantes, définies dans <stdio.h> :

  • IOFBF Tampon de taille fixe. L'écriture ou la lecture physique a lieu lorsque le tampon contient taille caractères.
  • IOLBF Tampon égal à une ligne. L'écriture ou la lecture physique a lieu lors de la rencontre du caractère 'nn' qui détermine la fin des lignes.
  • IONBF Pas de tampon. Une écriture ou une lecture physique a lieu à chaque lecture ou écriture logique. L'argument taille indique la taille voulue pour le tampon. L'argument tampon, s'il n'est pas NULL, est censé pointer un espace (de taille au moins égale à taille) qui sera utilisé comme tampon. Si tampon est NULL, alors la fonction alloue un espace propre.

VII-A-2. Les unités standard d'entrée-sortie

Sans que le programmeur n'ait à prendre aucune disposition particulière, l'exécution d'un programme commence avec trois flots de texte automatiquement ouverts par le système. Ils sont connectés (on dit affectés) aux organes d'entrée-sortie les plus « naturels » par rapport à la manière dont on utilise l'ordinateur. Ces fichiers sont déclarés dans <stdio.h>, de la manière suivante :

Sélectionnez

FILE *stdin, *stdout, *stderr;
  • stdin est l'unité standard d'entrée. Elle est habituellement affectée au clavier du poste de travail
  • stdout est l'unité standard de sortie. Elle est habituellement affectée à l'écran du poste de travail
  • stderr est l'unité standard d'affichage des erreurs. Elle est aussi affectée à l'écran du poste de travail

Les fonctions de lecture (resp. d'écriture) qui ne spécifient pas un autre flot utilisent stdin (resp. stdout).

Remarque. L'utilisation des unités standard apparait encore plus intéressante lorsque l'on sait que dans UNIX et plusieurs autres systèmes d'exploitation il est permis de réaffecter les unités standard lors d'une exécution du programme, sans qu'il faille recompiler ce dernier. En UNIX et MS-DOS cela fonctionne de la manière suivante : supposons que prog1 soit un programme qui n'utilise que les unités standard stdin et stdout. Activé par la commande prog1, il lira ses données au clavier et affichera ses résultats à l'écran. Mais s'il est lancé par la commande

Sélectionnez

prog1 < fichier1 (resp. : prog1 > fichier2)

alors le fichier fichier1 (resp. fichier2) remplacera le clavier (resp. l'écran). De manière similaire, supposons que prog2 soit un autre programme qui fait ses entrées-sorties de la même manière. Alors la commande

Sélectionnez

prog1 | prog2

active en parallèle ces deux programmes avec la sortie standard de prog1 connectée à l'entrée standard de prog2 (les sorties de prog1 sont lues et exploitées par prog2 au fur et à mesure qu'elles sont produites par prog1). Bien entendu, tout cela se combine. Par exemple, la commande

Sélectionnez

prog1 < fichier1 	| prog2 > fichier2

active prog1 et prog2. Les données de prog1 sont lues dans fichier1, ses résultats sont fournis à titre de données à prog2, dont les résultats sont écrits dans fichier2.

VII-B. Lecture et écriture textuelles

VII-B-1. Lecture et écriture de caractères et de chaines

Les fonctions de lecture-écriture les plus simples effectuent la lecture ou l'écriture d'un caractère isolé ou bien la lecture ou l'écriture d'une ligne. Pour la lecture nous avons :

Sélectionnez

int fgetc(FILE *flot)

Renvoie le caractère suivant sur le flot indiqué, ou EOF si la fin du fichier est atteinte ou si une erreur survient. C'est une vraie fonction.

Sélectionnez

int getc(FILE *flot)

Fait la même chose que fgetc mais c'est peut être macro.

L'intérêt d'utiliser une macro réside dans l'efficacité supérieure des (petites) macros par rapport aux (petites) fonctions 56. Il ne faut pas oublier cependant qu'il y a deux cas ou les macros ne peuvent pas être utilisées :

  • lorsque l'argument a un effet de bord. Une expression comme
    Sélectionnez

*s++ = fgetc(table_de_fichiers[i++])

    est correcte (mais peu utilisée), tandis que

    Sélectionnez

*s++ = getc(table_de_fichiers[i++])

    est certainement erronée, car ayant un effet indéfini (on ne peut pas dire combien de fois i sera incrémenté) ;

  • lorsqu'il faut obtenir l'adresse de l'opération en question, pour la passer comme argument d'une autre fonction. Par exemple, si getc est une macro, un appel comme appliquer(getc, fichier) sera certainement trouvé incorrect par le compilateur.
    Sélectionnez

int getchar(void)
getchar() équivaut à getc(stdin) ;
char *fgets(char *s, int n, FILE *flot)

    Lecture d'une ligne en veillant à ne pas déborder. Plus précisément, cette fonction lit des caractères sur le flot indiqué et les place dans l'espace pointé par s. Elle s'arrête lorsqu'elle a lu n¡1 caractères ou lorsqu'elle a rencontré un caractère 'nn' de fin de ligne (ce caractère est copié dans le tableau). Un caractère 'n0' est ajouté à la fin du tableau. La fonction rend s, ou NULL si une erreur ou la fin du fichier a été rencontrée.

    Lisez aussi la remarque faite ci-après à propos de la fonction gets.

    Sélectionnez

char *gets(char *s)

    Lecture d'une ligne sur le flot stdin. Les caractères lus sont rangés dans l'espace pointé par s. Elle renvoie le même résultat que fgets mais, contrairement à cette dernière

  • il n'y a pas de test de débordement 57 ;
  • le caractère de fin de ligne est remplacé par 'n0'.

    Attention. L'argument de gets doit être l'adresse d'un espace disponible (accessible en écriture) et de taille suffisante, dans lequel gets puisse ranger les caractères qui seront lus.

Exemples. Pour illustrer ces fonctions, voici deux exercices d'école. D'une part, l'écriture d'une fonction analogue à gets en utilisant getchar :

Sélectionnez

char *myGets(char *s) {
int c, i = 0;
c = getchar();

while (c != '\n') {
    if (c == EOF)
        return NULL;
    s[i++] = c;
    c = getchar();
}
s[i] = '\0';
return s;
}

D'autre part, l'écriture d'une fonction analogue à getchar (version fonction) en utilisant gets :

Sélectionnez

int myGetchar(void) {
    static char tampon[256] = "";
    static char *pos = tampon;
    if (*pos == 0) {
        if (gets(tampon) == NULL)
            return EOF;
        strcat(tampon, "\n");
        pos = tampon;
    }
    return *pos++;
}

Voyons maintenant les fonctions d'écriture :

Sélectionnez

int fputc(int caractere, FILE *flot)

écrit le caractere indiqué sur le flot indiqué. Renvoie le caractère écrit ou EOF si une erreur survient.

C'est une vraie fonction.

Sélectionnez

int putc(int caractere, FILE *flot)

Fait la même chose que fputc mais peut être une macro. Voir à ce propos les remarques faites à l'occasion de l'introduction de getc.

Sélectionnez

int putchar(int caractere)
putchar(c) équivaut à putc(c, stdout).
int fputs(const char *s, FILE *flot)

écrit la chaine s sur le flot indiqué. La fonction renvoie EOF en cas d'erreur, une valeur ¸ 0 dans les autres cas.

Sélectionnez

int puts(const char *s)

Cette fonction écrit la chaine s sur le flot stdout. Elle renvoie le même résultat que fputs. Contrairement à fputs un caractère 'nn' est ajouté à la suite de la chaine.

Sélectionnez

int ungetc(int c, FILE *flot)

« Délecture » du caractère c. Cette fonction remet le caractère c dans le flot indiqué, ou il sera trouvé lors de la prochaine lecture. Sur chaque flot, seule la place pour un caractère « délu » est garantie. On ne peut pas « délire » le caractère EOF.

Cette opération incarne donc l'idée de restitution (à l'unité d'entrée, qui en est le fournisseur) d'un caractère lu à tort. C'est une manière de résoudre un problème que pose l'écriture d'analyseurs lexicaux, ou la fin de certaines unités lexicales est indiquée par un caractère qui les suit sans en faire partie. Exemple : la lecture d'un nombre entier non négatif :

Sélectionnez

int lirentier(FILE *flot) { /* lecture d'un entier (simpliste) */
int c, x = 0;
c = getc(flot);
while ('0' <= c && c <= '9') {
x = 10 * x + c - '0';
c = getc(flot);
}
/* ici, c est le caractère suivant le nombre */
ungetc(c, flot);
return x;
}

56Il s'agit d'entrées-sorties tamponnées, c'est-à-dire de simples transferts entre mémoires. S'il s'était agi d'opérations physiques, le gain de temps obtenu en utilisant getc à la place de fgetc aurait été négligeable devant le temps requis par l'entrée-sortie elle-même.

57Ainsi, gets lit des caractères jusqu'à la rencontre de 'nn', sans se préoccuper de savoir si l'espace dans lequel ces caractères sont rangés est de taille suffisante pour les recevoir ; autrement dit, tout appel de gets peut entrainer un débordement de mémoire. C'est la raison pour laquelle on dit que tout programme comportant ne serait-ce qu'un appel de gets est bogué.

VII-B-2. Ecriture avec format printf

La fonction d'écriture sur l'unité de sortie standard (en principe l'écran du poste de travail) avec conversion et mise en forme des données est :

Sélectionnez

printf( format , expr1 , expr2 , ... exprn )

format est une chaine qui joue un rôle particulier. Elle est formée de caractères qui seront écrits normalement, mélangés à des indications sur les types des expressions expr1, expr2, ... exprn et sur l'aspect sous lequel il faut écrire les valeurs de ces expressions.

Cela fonctionne de la manière suivante : la fonction printf parcourt la chaine format et reconnait certains groupes de caractères comme étant des spécifications de format. C'est le caractère % qui déclenche cette reconnaissance. La fonction printf recopie sur le flot de sortie tout caractère qui ne fait pas partie d'une telle spécification. La ieme spécification détermine la manière dont la valeur de expri sera écrite. Les spécifications de format reconnues par la fonction printf sont expliquées dans la table 4 (page 91).

Notez que seuls les éléments 1 (le caractère %) et 6 (la lettre qui indique le type de la conversion) sont obligatoires dans une spécification de format.

Exemple 1. Supposons qu'on ait donné la déclaration-initialisation

Sélectionnez

float x = 123.456;

Voici une liste d'appels de printf, avec l'affichage produit par chacun :

Sélectionnez

Appel 					Affichage obtenu
printf(">%f<", x); 			>123.456001<
printf(">%12f<", x); 			> 123.456001<
printf(">%12.2f<", x); 			> 123.46<
printf(">%.2f<", x); 			>123.46<
printf(">%-12.2f<", x); 		>123.46 <
printf(">%+-12.2f<", x); 		>+123.46 <
printf(">% -12.2f<", x); 		> 123.46 <
printf(">%12.2f<", x); 			> 123.46<
printf(">%012.2f<", x); 		>000000123.46<
printf(">%.0f<", x); 			>123<
printf(">%#.0f<", x); 			>123.<

Exemple 2. La liste suivante illustre l'utilisation de la largeur et de la précision pour les chaines. Supposons avoir donné les déclarations :

Sélectionnez

char *s = "ABCDEFG";
int l = -10, n = 5;

Voici une liste d'appels de printf, avec l'affichage produit par chacun :

Sélectionnez

Appel 							Affichage obtenu
printf("printf(">%5s<", s); 				>ABCDEFG<
printf("printf(">%10s<", s); 				> ABCDEFG<
printf("printf(">%-10s<", s); 				>ABCDEFG <
printf("printf(">%10.5s<", s); 				> ABCDE<
printf("printf(">%-10.5s<", s); 			>ABCDE <
printf("printf(">%.5s<", s); 				>ABCDE<
printf("printf(">%*.*s<", l, n, s); 			>ABCDE <

La largeur du champ (cf. tableau 4, nº3) peut être indiquée par le caractère * à la place d'un nombre. Elle est alors définie par la valeur de l'argument dont c'est le tour, qui doit être de type entier. Il en est de même pour la spécification de la précision (cf. tableau 4, nº4). Par exemple, i et j étant des variables entières et x une variable flottante, l'expression

Sélectionnez

printf("%*.*f", i, j, x);

écrit la valeur de x sur un nombre de caractères égal à la valeur de i, avec un nombre de chiffres après la virgule égal à la valeur de j.

Tab. 4 - Spécifications de format pour printf, fprintf et sprintf
Tab. 4 - Spécifications de format pour printf, fprintf et sprintf

VII-B-3. Lecture avec format scanf

La fonction de lecture avec format à l'unité d'entrée standard (en principe le clavier du poste de travail) est

Sélectionnez

scanf( format , adresse1 , adresse2 , ... adressen )

L'argument format est une chaine qui indique la manière de convertir les caractères qui seront lus ; adresse1, adresse2, ... adressen indiquent les variables devant recevoir les données lues. Ce sont des « sorties » de la fonction scanf, par conséquent il est essentiel que ces arguments soient des adresses de variables à lire.

La fonction scanf constituant un vrai analyseur lexical, les données lues ont de nombreuses occasions d'être incorrectes et on peut s'attendre à ce que scanf soit d'un maniement difficile. L'expérience ne déçoit pas cette attente ! Voici un bon conseil : si un programme ne fonctionne pas comme il le devrait, commencez par vérifier les valeurs lues par la fonction scanf (par exemple avec des appels de printf suivant immédiatement les appels de scanf).

Les spécifications de format reconnues par la fonction scanf sont expliquées dans la table 5 (page 93). Fonctionnement : scanf parcourt le format. Elle rend la main lorsque la fin du format est atteinte ou sur une erreur. Jusque-là :

  • Tout caractère ordinaire du format, c'est-à-dire qui n'est ni un caractère d'espacement (blanc, tabulation) ni un caractère faisant partie d'une spécification de format (commençant par %), doit s'identifier au caractère courant du flot d'entrée. Si cette identification a lieu, le caractère courant est lu, sans être rangé dans aucune variable, et le parcours du format se poursuit. Si l'identification n'a pas lieu, l'activation de scanf se termine.
  • Les spécifications de format commencent par %. Elles indiquent la manière d'analyser les caractères lus sur le flot d'entrée et de ranger les valeurs ainsi obtenues. Voir le tableau 5.
  • Dans le format, les caractères ordinaires et les spécifications peuvent être séparés entre eux par des caractères d'espacement. Le nombre de ces espacements est sans importance, mais leur présence indique que les données correspondantes peuvent être séparées dans le flot d'entrée par un nombre quelconque de caractères d'espacement ou de fin de ligne.
  • S'il n'y a pas d'espacement, dans le format, entre les caractères ordinaires ou les spécifications, alors les données correspondantes dans le flot d'entrée doivent être adjacentes.
  • Cependant, les espacements au début des nombres sont toujours sautés.

On peut mélanger des appels de scanf et d'autres fonctions de lecture. Chaque appel d'une telle fonction commence par lire le premier caractère que l'appel précédent n'a pas « consommé ».

Tab. 5 - Spécifications de format pour scanf, fscanf et sscanf
Tab. 5 - Spécifications de format pour scanf, fscanf et sscanf

Exemple 1. Lecture d'un entier :

Sélectionnez

scanf("%d", &x);

Exemple 2. Lecture de deux entiers séparés par une virgule et encadrés par une paire de parenthèses :

Sélectionnez

scanf("(%d,%d)", &amp;x, &amp;y);

Données correctes :

(22,33) ou ( 22, 33)

(des blancs, mais uniquement devant les nombres).

Données incorrectes :

( 22 , 33 )

(trop de blancs).

Exemple 3. Comme ci-dessus mais avec une syntaxe plus souple (les blancs sont tolérés partout) :

Sélectionnez

scanf(" (%d ,%d )", &x, &y);

Toutes les données de l'exemple précédent sont correctes. Exemple de donnée incorrecte :

( 22 ; 33 )

Exemple 4. Lecture d'un caractère 58 :

Sélectionnez

char calu;
...
scanf("%c", &calu);

Remarque. Nous avons indiqué par ailleurs qu'en C on range souvent les caractères dans des variables de type int afin de permettre la mémorisation d'une valeur « intruse », EOF. Cependant, notez bien qu'ici nous sommes obligés de déclarer la variable calu de type char ; le programme

Sélectionnez

int calu;
...
scanf("%c", &calu);

aurait été incorrect (bien qu'il puisse fonctionner correctement sur certains systèmes), car on y fournit à scanf l'adresse d'un entier (fait de plusieurs octets) pour y ranger un caractère, alors que scanf, d'après l'analyse du format, « croit » recevoir l'adresse d'un char (un octet). Or on ne doit pas faire d'hypothèse sur la manière dont sont organisés les octets qui composent un entier, le langage C ne précisant rien à ce sujet. Une autre solution correcte aurait été la suivante :

Sélectionnez

int calu;
char tmp;
...
scanf("%c", &tmp);
calu = tmp;

Exemple 5. Lecture d'un nombre entier et du caractère le suivant immédiatement, quel qu'il soit :

Sélectionnez

int nblu;
char calu;
...
scanf("%d%c", &nblu, &calu);

Exemple 6. Lecture d'un nombre entier et du premier caractère non blanc le suivant :

Sélectionnez

int nblu;
char calu;
...
scanf("%d %c", &nblu, &calu);

58Rappelons que fgetc, getc et getchar font ce travail bien plus simplement.

VII-B-4. A propos de la fonction scanf et des lectures interactives

A propos de « sauter les blancs ». Nous avons dit que les spécifications qui constituent le format peuvent être séparées entre elles par un ou plusieurs blancs, ce qui indique que les données à lire correspondantes peuvent alors être séparées par un nombre quelconque de blancs.

Pour la fonction scanf, les blancs dans le format se traduisent donc par l'ordre « à présent, sautez tous les caractères blancs qui se présentent ». Or la seule manière pour l'ordinateur de s'assurer qu'il a bien sauté tous les caractères blancs consiste à lire (sans le consommer) le premier caractère non blanc qui les suit. Si la lecture se fait dans un fichier, cela ne pose pas de problème, les caractères à lire existant tous dès le départ. Mais cela peut créer des situations confuses dans le cas d'une lecture interactive mettant en œuvre le clavier et un opérateur vivant. Exemple :

Sélectionnez

int x, y = 33;
...
printf("donne x : ");
scanf(" %d ", &x); 		/* aïe... */
printf("donne y : ");
scanf("%d", &y);
printf("x = %d, y = %d\n", x, y);

Parce qu'on a écrit, peut-être sans faire attention, des blancs après le format %d, le premier appel de scanf ci-dessus signifie « lisez un entier et tous les blancs qui le suivent ». L'opérateur constatera avec étonnement que pour que son premier nombre sont pris en compte il est obligé de composer le deuxième !

Exemple de dialogue (le signe ↵ représentant la frappe de la touche « Entrée ») :

Sélectionnez

donne x : 11↵
↵
↵ la machine ne réagit pas... ! ?
22↵
donne y : x = 11, y = 22

Notez que faire suivre le premier nombre d'un caractère non blanc conventionnel n'est pas une bonne solution car, ce caractère n'étant pas consommé, il fait échouer la lecture du second nombre :

Sélectionnez

donne x : 11↵
donne y : x = 11, y = 33

Leçon à retenir : dans le format de scanf, les blancs ne sont pas de la décoration, ils jouent un rôle bien précis.

A propos de la lecture d'un caractère. Une autre situation génératrice de confusion est celle ou des lectures de nombres sont suivies par des lectures de caractères ou de chaines. Par exemple, le programme naïf suivant lit des nombres les uns à la suite des autres et affiche le carré de chacun :

Sélectionnez

int x, c;
...
do {
    printf("donne un nombre : ");
    scanf("%d", &x); 	/* lecture d'un nombre */
    printf("%d ^ 2 = %d\n", x, x * x);
    printf("un autre calcul (O/N) ? ");
    c = getchar(); 		/* lecture d'un caractère */
}
while (c == 'O');
printf("au revoir...");\end{verbatim}

Voici un dialogue (la réplique de l'opérateur est soulignée, le signe ↵ représente la frappe de la touche « Entrée ») :

Sélectionnez

donne un nombre : 14↵
14 ^ 2 = 196
un autre calcul (O/N) ? au revoir...

La machine n'a pas attendu la réponse (O ou N) de l'opérateur... ! Que s'est-il passé ? L'opérateur a composé un nombre suivi immédiatement d'un caractère fin-de-ligne (traduction de la frappe de la touche « Entrée »).

Cette fin de ligne est inévitable mais n'a pas été consommée par scanf et est restée disponible pour des lectures ultérieures. L'appel de getchar (qui, dans l'esprit du programmeur, aurait dû provoquer une nouvelle lecture physique) trouve cette fin-de-ligne et l'affecte à c comme réponse, ce qui fait quitter la boucle. Ainsi, pour que ce programme se comporte comme son programmeur attend il faudrait qu'à la suite de l'invite « donne un nombre : », l'utilisateur saisisse un nombre immédiatement suivi d'un caractère O ou N qui est la réponse à une question qui ne lui a pas encore été posée !

Cela n'est pas raisonnable, bien sûr. La bonne manière de faire consiste à vider le tampon d'entrée avant de procéder à une lecture de caractère. Dans beaucoup d'environnements on peut faire cela avec la fonction

Sélectionnez

fflush :
int x, c;
char s[BUFSIZ];
...
do {
    printf("donne un nombre : ");
    scanf("%d", &x);
    printf("%d ^ 2 = %d\n", x, x * x);
    printf("un autre calcul (O/N) ? ");
    /* ici, le tampon de stdin contient (au moins) un 'nn' */
    fflush(stdin);
    /* maintenant le tampon est vide */
    c = getchar();
}
while (c == 'O');
printf("au revoir...");
...

Si la fonction fflush est inopérante sur le flot stdin (un tel comportement est permis par la norme du langage C) alors on peut résoudre le même problème en faisant des appels répétés de la fonction getchar, de manière à consommer tous les caractères restant dans le tampon jusqu'à la fin-de-ligne, ce caractère devant être consommé lui aussi. L'exemple ci-dessus devient

Sélectionnez

...
scanf("%d", &x);
printf("%d ^ 2 = %d\n", x, x * x);
printf("un autre calcul (O/N) ? ");
/* ici, le tampon de stdin contient (au moins) un 'nn' */
while (getchar() != '\n')
;
/* maintenant le tampon est vide */
c = getchar();
...

VII-B-5. Les variantes de printf et scanf

Voici la famille au complet, comme elle est déclarée (en syntaxe ANSI) dans le fichier <stdio.h> :

• printf

Sélectionnez

int printf(const char *format, ... )

Nous avons décrit cette fonction. Ajoutons seulement qu'elle renvoie le nombre de caractères effectivement écrits (on utilise rarement cette information) ou un nombre négatif en cas d'erreur.

• fprintf

Sélectionnez

int fprintf(FILE *flot, const char *format, ... )

Comme printf mais sur le flot indiqué (à la place de stdout). Ainsi

Sélectionnez

printf( format , exp1 , ... , expk )

équivaut à

Sélectionnez

fprintf(stdout, format , exp1 , ... , expk )
int sprintf(char *destination, const char *format, ... )

Cette fonction effectue les mêmes mises en forme que ses deux sœurs, avec la différence que les caractères produits ne sont pas ajoutés à un flot, mais sont concaténés ensemble dans la chaine destination. Elle permet donc de dissocier les deux fonctionnalités de printf, transcodage et écriture des données, afin d'utiliser la première sans la deuxième.

• sscanf

Sélectionnez

int scanf(const char *format, ... )

Nous avons décrit cette fonction. Ajoutons qu'elle renvoie EOF si une fin de fichier ou une erreur a empêché toute lecture ; autrement elle rend le nombre de variables lues avec succès. Cette indication fournit une manière bien pratique pour détecter, dans les programmes simples, la fin d'une série de données. Exemple :

Sélectionnez

for(;;) {
    printf("donne un nombre : ");
    if (scanf("%d", &x) < 1)
        break;
    /* exploitation du nombre lu */
}
printf("au revoir...");

La boucle précédente sera arrêtée par n'importe quel caractère non blanc ne pouvant pas faire partie d'un nombre.

• fscanf

Sélectionnez

int fscanf(FILE *flot, const char *format, ... )

Comme scanf mais sur le flot indiqué (à la place de stdin). Ainsi

Sélectionnez

scanf( format , exp1 , ... , expk )

équivaut à

Sélectionnez

fscanf(stdin, format , exp1 , ... , expk )
int sscanf(const char *source, const char *format, ... )

Cette fonction effectue la même analyse lexicale que ses sœurs, avec la particularité que le texte analysé n'est pas pris dans un flot, mais dans la chaine source.

VII-C. Opérations en mode binaire

VII-C-1. Lecture-écriture

• fread

Sélectionnez

size t fread(void *destination, size t taille, size t nombre, FILE *flot)

Cette fonction essaye de lire sur le flot indiqué nombre objets, chacun ayant la taille indiquée, et les copie les uns à la suite des autres dans l'espace pointé par destination. Elle renvoie le nombre d'objets effectivement lus, qui peut être inférieur au nombre demandé, à cause de la rencontre de la fin du fichier 59, d'une erreur, etc.

• fwrite

Sélectionnez

size t fwrite(const void *source, size t taille, size t nombre, FILE *flot)

Cette fonction écrit les nombre objets, chacun ayant la taille indiquée, qui se trouvent les uns à la suite des autres à l'adresse indiquée par source. Elle renvoie le nombre d'objets écrits, qui peut être inférieur au nombre demandé (en cas d'erreur).

59Lorsqu'un fichier est lu par des appels de la fonction fread, comparer avec zéro le nombre d'objets effectivement lus est la manière la plus pratique de détecter la fin du fichier.

VII-C-2. Positionnement dans les fichiers

• fseek

Sélectionnez

int fseek(FILE *flot, long deplacement, int origine)

Cette fonction positionne le pointeur du fichier associé au flot indiqué. La première lecture ou écriture ultérieure se fera à partir de la nouvelle position. Celle-ci est obtenue en ajoutant la valeur de déplacement à une valeur de base qui peut être la position courante, la fin ou le début du fichier. C'est l'argument origine qui indique de quelle base il s'agit ; la valeur de cet argument doit être une des constantes (définies dans <stdio.h>) :

  • SEEK SET : base = le début du fichier
  • SEEK CUR : base = la position courante
  • SEEK END : base = la fin du fichier

La fonction renvoie zéro en cas de succès, une valeur non nulle en cas d'erreur. Sur un fichier de texte, il vaut mieux utiliser getpos et setpos.

Un appel de cette fonction sur un fichier en écriture produit la vidange du tampon (fseek implique fflush). Lire ci-après les explications sur l'accès relatif aux fichiers.

• frewind

Sélectionnez

void rewind(FILE *flot)

Positionnement au début du fichier du pointeur de fichier associé au flot indiqué. Un appel de cette fonction équivaut à fseek(flot, 0L, SEEK SET) suivi de la mise à zéro de tous les indicateurs d'erreur.

• fgetpos

Sélectionnez

void fgetpos(FILE *flot, fpos t *ptr)

Place dans ptr la position courante dans le flot indiqué en vue de son utilisation par setpos. Le type fpos t (défini dans <stdio.h>) a une taille suffisante pour contenir une telle information.

• fsetpos

Sélectionnez

void fsetpos(FILE *flot, const fpos t *ptr)

La valeur de ptr provenant d'un appel de getpos, cette fonction positionne le flot indiqué à l'emplacement correspondant.

L'accès relatif aux éléments des fichiers. La principale application des fonctions de positionnement est la réalisation de ce qu'on appelle l'accès relatif 60 ou parfois accès direct aux composantes d'un fichier : l'accès à une composante (on dit un enregistrement) que ce soit pour le lire ou pour l'écrire, à partir de la donnée de son rang dans le fichier sans être obligé d'accéder au préalable aux enregistrements qui le précèdent.

Pour donner une signification utilisable à la notion de « neme article », il faut que le fichier puisse être vu comme une suite d'articles de même taille (mais la bibliothèque C ignorera ce fait ; le fichier restera une suite d'octets). Cela s'obtient généralement en déclarant une structure qui représente un article, selon le modèle :

Sélectionnez

typedef struct {
...

champs dont se composent tous les articles

Sélectionnez

...
} ARTICLE;...

Un fichier destiné à être utilisé en accès relatif sera déclaré normalement, et ouvert de fa»con à autoriser les lectures et les écritures :

Sélectionnez

FILE *fichier;
...
fichier = fopen(nom , "rb+");

Si les articles sont numérotés à partir de zéro, l'opération « lecture de l'enregistrement de rang n » se programme alors selon le schéma suivant :

Sélectionnez

ARTICLE article;
...
fseek(fichier, n * sizeof(ARTICLE), SEEK_SET);
fread( & article, sizeof(ARTICLE), 1, fichier);
...
/* exploitation des valeurs des champs de article */
...

L'opération « écriture de la composante de rang n » obéit au schéma

Sélectionnez

ARTICLE article;
...
/* affectation des champs de article */
...
fseek(fichier, n * sizeof(ARTICLE), SEEK_SET);
fwrite( & article, sizeof(ARTICLE), 1, fichier);
...

Attention. La possibilité d'effectuer des lectures et des écritures sur le même fichier pose des problèmes dans la gestion des tampons associés au fichier que les fonctions de la bibliothèque standard ne résolvent pas. Ainsi chaque fois qu'une série de lectures sur un fichier va être suivie d'une série d'écritures sur le même fichier, ou réciproquement, il appartient au programmeur de prévoir l'écriture effective du tampon, soit par un appel explicite de la fonction fflush, soit par un appel d'une fonction de positionnement comme fseek. Notez que cette contrainte est satisfaite si on pratique des accès relatifs selon les schémas indiqués ci-dessus, puisqu'il y a toujours un appel de fseek entre deux accès au fichier, quel que soit leur mode.

60L'accès relatif s'oppose à l'accès séquentiel, dans lequel une composante ne peut être lue ou écrite autrement qu'à la suite de la lecture ou de l'écriture de la composante qui la précède.

VII-D. Exemples

Les programmes suivants sont assez naïfs ; ils se proposent d'illustrer les fonctions de traitement de fichiers, aussi bien pour les fichiers de texte que pour les fichiers binaires.

VII-D-1. Fichiers "en vrac"

Le programme suivant fait une copie identique d'un fichier (ce programme n'est pas bien utile, généralement une fonction du système d'exploitation fait ce travail) :

Sélectionnez

#include <stdio.h>
#define PAS_D_ERREUR 		0 /* codes conventionnels */
#define ERREUR_OUVERTURE 	1 /* à l'usage du système */
#define ERREUR_CREATION 	2 /* d'exploitation */
FILE *srce, *dest;

main() {
    char tampon[512];
    int nombre;

    printf("source : ");
    gets(tampon);
    srce = fopen(tampon, "rb");
    if (srce == NULL)
        return ERREUR_OUVERTURE;

    printf("destination : ");
    gets(tampon);
    dest = fopen(tampon, "wb");
    if (dest == NULL)
    return ERREUR_CREATION;

    while ((nombre = fread(tampon, 1, 512, srce)) > 0)
        fwrite(tampon, 1, nombre, dest);

    fclose(dest);
    fclose(srce);
    return PAS_D_ERREUR;
}

L'essentiel de ce programme est sa boucle while. Elle se compose d'un appel de fread demandant la lecture de 512 octets, suivi d'un appel de fwrite pour écrire les nombre octets effectivement lus. Supposons que le fichier à copier comporte N octets ; ces deux opérations seront exécutées N 512 fois avec nombre égal à 512 puis

(sauf si N est multiple de 512) une dernière fois avec nombre égal au reste de la division de N par 512.

VII-D-2. Fichiers binaires et fichiers de texte

L'objet du programme suivant est la constitution d'un fichier d'articles à partir d'un fichier de texte qui se présente comme une répétition du groupe suivant :

  • un nom et un prénom sur la même ligne ;
  • une adresse sur la ligne suivante ;
  • un entier seul sur une ligne, exprimant un certain « nombre de passages » (à un péage autoroutier).

Exemple

Sélectionnez

TOMBAL Pierre
Rue de la Gaiete de Vivre
    10

MENSOIF Gerard
Allee 'Les verts'
    20
        etc.

Le programme est indépendant de la nature du fichier de texte, qui peut être aussi bien le clavier d'un terminal qu'un fichier existant par ailleurs.

Sélectionnez

/* constitution d'un fichier d'articles */
/* à partir d'un fichier de texte */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PAS_D_ERREUR 		0
#define ERREUR_OUVERTURE 	1
#define ERREUR_CREATION 	2
#define ERREUR_ECRITURE 	3
#define ERREUR_FERMETURE 	4

struct 					/* un article du fichier */
    {
    char nom[32], prenom[32];
    char adresse[80];
    int nombre_de_passages;
    } art;

FILE *srce; 		/* fichier de texte */
FILE *dest; 		/* fichier binaire */

main() {
    char nom[256];

    printf("source : ");
    gets(nom);
    if ((srce = fopen(nom, "r")) == NULL)
        exit(ERREUR_OUVERTURE);
    printf("destination : ");
    gets(nom);
    if ((dest = fopen(nom, "wb")) == NULL)
        exit(ERREUR_CREATION);

for (;;) {
    if (fscanf(srce, " %s %s ", art.nom, art.prenom) != 2)
        break;
    fgets(art.adresse, 80, srce);

                            /* enlevons le '\n' : */
    art.adresse[strlen(art.adresse) - 1] = '\0';
    fscanf(srce, " %d ", &art.nombre_de_passages);
    if (fwrite(&art, sizeof art, 1, dest) != 1)
        exit(ERREUR_ECRITURE);
}
if (fclose(dest) != 0)
    exit(ERREUR_ECRITURE);
exit(PAS_D_ERREUR);
}

Notez les diverses fonctions qui ont été utilisées pour lire du texte. Le nombre de passages est lu par scanf, car cette fonction est la seule qui convertit les nombres. Le nom et le prénom sont lus par scanf aussi, car cela nous arrange bien que les blancs soient pris pour des séparateurs et enlevés. Mais l'adresse est lue par gets, car elle peut contenir des blancs qui ne doivent pas être supprimés.

VII-D-3. Fichiers en accès relatif

Dans le programme précédent nous avons vu un fichier binaire organisé en articles et traité séquentiellement (chaque article étant écrit à la suite du précédent). Voyons un exemple ou ce même fichier est traité en accès relatif.

Imaginons que le fichier créé à l'exemple précédent soit le fichier des abonnés à un certain péage, et que nous disposions par ailleurs d'un fichier binaire contenant les numéros des abonnés qui ont emprunté ce péage durant une période donnée. On nous demande d'écrire un programme pour incrémenter la valeur du champ nbr passages de chaque abonné concerné.

Sélectionnez

#include <stdio.h>

#define PAS_D_ERREUR 				0
#define ERREUR_OUVERTURE_ABONNES 	1
#define ERREUR_OUVERTURE_PASSAGES 	2

struct
    {
    char nom[32], prenom[32];
    char adresse[80];
    int nombre_de_passages;
    } art;

    FILE *passages, *abonnes;

    main() {
        char nom[256];
        long n;

        printf("fichier des passages : ");
        if ((passages = fopen(gets(nom), "rb")) == NULL)
            exit(ERREUR_OUVERTURE_PASSAGES);
        printf("fichier des abonnes : ");
        if ((abonnes = fopen(gets(nom), "rb+")) == NULL)
            exit(ERREUR_OUVERTURE_ABONNES);

        for (;;) {
            if (fread(&n, sizeof n, 1, passages) != 1)
                break;
            fseek(abonnes, n * sizeof art, SEEK_SET);
            fread(&art, sizeof art, 1, abonnes);
            art.nombre_de_passages++;
            fseek(abonnes, n * sizeof art, SEEK_SET);
            fwrite(&art, sizeof art, 1, abonnes);
            }

        fclose(abonnes);
        exit(PAS_D_ERREUR);
}

VII-E. Les fichiers de bas niveau d'UNIX

Les fichiers de bas niveau du système UNIX ne sont pas fondamentalement distincts des flots que l'on vient de décrire. En fait, les flots et les fichiers de bas niveau sont deux manières de voir depuis un programme les mêmes entités de base (les fichiers). Les fichiers de bas niveau sont la manière habituelle de réaliser les fichiers binaires lorsqu'on ne possède pas un C ANSI.

Dans le système UNIX chaque processus dispose, pour gérer ses fichiers, d'une table de descripteurs de fichiers. Lorsque les fichiers sont gérés au plus bas niveau, ils sont représentés tout simplement par un indice dans cette table. Par exemple, les trois unités standard stdin, stdout et stderr sont respectivement représentées en tant que fichiers par les descripteurs d'indices 0, 1 et 2.

Les fonctions disponibles sont :

• open

Sélectionnez

int open(char *nom, int mode, int permissions)

Ouvre un fichier existant ayant le nom externe indiqué. L'argument mode doit avoir pour valeur une des constantes symboliques (définies dans <ioctl.h> ou dans <sys/file.h>) :

  • O RDONLY : ouverture en lecture seulement
  • O WRONLY : ouverture en écriture seulement
  • O RDWR : ouverture en lecture / écriture

Cette fonction renvoie le numéro du descripteur ouvert pour le fichier, ou un nombre négatif en cas d'erreur. Pour l'argument permissions voir ci-dessous.

• creat

Sélectionnez

int creat(char *nom, int permissions)

Crée un fichier nouveau, ayant le nom indiqué, et l'ouvre en écriture. Comme pour la fonction open, l'argument permissions indique que le droit de lecture, le droit d'écriture ou le droit d'exécution est accordé ou non au propriétaire du fichier, aux membres de son groupe de travail ou à tous les utilisateurs du système. Cela fait trois groupes de trois bits, soit un nombre qui s'écrit particulièrement bien en octal.

Par exemple :

Sélectionnez

fic = creat("tmp/monfic", 0751);

crée un fichier nouveau et lui accorde les permissions :

 | lecture | écriture | exécution | en octal
propriétaire | 1 | 1 | 1 | 7
groupe | 1 | 0 | 1 | 5
autres | 0 | 0 | 1 | 1

Lit nombre octets depuis le fichier indiqué et les range à partir de l'adresse indiquée. Renvoie le nombre d'octets effectivement lus, qui peut être inférieur à nombre (fin de fichier, erreur de lecture, etc.).

• write

Sélectionnez

int write(int fichier, void *adresse, int nombre)

Ecrit les nombre octets qui se trouvent à l'adresse indiquée dans le fichier indiqué. Rend le nombre d'octets effectivement écrits, qui peut être inférieur à nombre (erreur d'écriture, etc.).

• close

Sélectionnez

int close(int descfic)

Ferme le fichier indiqué.

• lseek

Sélectionnez

long lseek(int fichier, long rang, int origine)

Positionne le fichier indiqué sur l'octet ayant le rang indiqué. Ce rang exprime une position...

Sélectionnez

...relative au début du fichier (si origine = 0)
...relative à la position courante (si origine = 1)
...relative à la fin du fichier (si origine = 2)

Exemple. Voici la version UNIX (et, de plus, non ANSI) du programme, donné plus haut, qui effectue une copie identique d'un fichier quelconque. Au sujet de l'utilisation qui est faite des arguments de main voir la section 8.3.1.

Sélectionnez

main(int argc, char *argv[]) {
    int srce, dest, n;
    char tampon[512];
    if (argc < 3
            || (srce = open(argv[1], 0, 0777)) < 0
            || (dest = creat(argv[2], 0777)) < 0)
        return 1;
    while ((n = read(srce, tampon, 512)) > 0)
        write(dest, tampon, n);
    close(dest);
    close(srce);
    return 0;
}


Java et Eclipse tutoriel JM_Doudoux

Le langage C

  • VIII. Autres éléments du langage C
    • VIII-A. Le préprocesseur
      • VIII-A-1. Inclusion de fichiers
      • VIII-A-2. Définition et appel des "macros"
      • VIII-A-3. Compilation conditionnelle
    • VIII-B. La modularité de C
      • VIII-B-1. Fichiers en-tête
      • VIII-B-2. Exemple : stdio.h
    • VIII-C. Deux ou trois choses bien pratiques...
      • VIII-C-1. Les arguments du programme principal
      • VIII-C-2. Branchements hors fonction : setter.h
      • VIII-C-3. Interruptions : signal.h
    • VIII-D. La bibliothèque standard
      • VIII-D-1. Aide à la mise au point : assert.h
      • VIII-D-2. Fonctions utilitaires : stdlib.h
      • VIII-D-3. Traitement de chaines : string.h
      • VIII-D-4. Classification des caractères : ctype.h
      • VIII-D-5. Fonctions mathématiques : math.h
      • VIII-D-6. Limites propres à l'implémentation : limits.h, float.h

VIII. Autres éléments du langage C

Cette section regroupe un ensemble de notions ayant en commun que leur intérêt apparait surtout à l'occasion de l'écriture de gros programmes. Ce sont des éléments importants du langage mais, sauf quelques exceptions, on peut s'en passer aussi longtemps qu'on limite la pratique de C à des exercices de petite taille.

VIII-A. Le préprocesseur

Le préprocesseur transforme le texte source d'un programme avant que la compilation ne commence.

Généralement associées au processus de la compilation, ses actions semblent en faire partie, mais il faut sa- voir que le préprocesseur est un programme séparé, largement indépendant du compilateur. En particulier le préprocesseur ne connait pas la syntaxe du langage C. Les transformations qu'il effectue sur un programme sont simplement des ajouts, des suppressions et des remplacements de morceaux de texte nullement astreints à correspondre aux entités syntaxiques du langage.

Les directives destinées au préprocesseur comportent, dans l'ordre :

  • un signe # (qui, en syntaxe originale, doit occuper la première position de la ligne)
  • un mot-clé parmi include, define, if, ifdef, ifndef, else et endif qui identifie la directive. Nous passerons sous silence quelques autres directives mineures comme pragma, line ou error
  • le corps de la directive, dont la structure dépend de la nature de celle-ci.

VIII-A-1. Inclusion de fichiers

La possibilité d'inclure des fichiers sources dans d'autres fichiers sources s'avère très utile, par exemple pour insérer dans chacun des fichiers séparés qui constituent un programme l'ensemble de leurs déclarations communes (types, structures, etc.). Ainsi ces définitions sont écrites dans un seul fichier, ce qui en facilite la maintenance.

Un cas particulier très important de ce partage des déclarations est celui des bibliothèques livrées avec le compilateur. Elles sont essentiellement composées de fonctions utilitaires déjà compilées dont seul le fichier objet est fourni. Pour que les appels de ces fonctions puissent être correctement compilés 61 dans des programmes qui les utilisent, un certain nombre de fichiers en-tête sont livrés avec les bibliothèques, comprenant

  • principalement, les prototypes des fonctions qui forment la bibliothèque ou qui incarnent une fonctionnalité particulière (les entrées - sorties, la gestion des chaines de caractères, la bibliothèque mathématique, etc.) ;
  • souvent, un ensemble de directives #define et de déclarations struct, union et typedef qui définissent les constantes et les types nécessaires pour utiliser la bibliothèque en question ;
  • parfois, quelques directives #include concernant d'autres fichiers en-tête lorsque les éléments définis dans le fichier en-tête en question ne peuvent eux-mêmes être compris sans ces autres fichiers ;
  • plus rarement, quelques déclarations de variables externes.

Cette directive possède deux formes :

Sélectionnez

#include "nom-de-fichier"

C'est la forme générale. Le fichier spécifié est inséré à l'endroit ou la directive figure avant que la compilation ne commence ; ce doit être un fichier source écrit en C. La chaine de caractères "nom-de-fichier" doit être un nom complet et correct pour le système d'exploitation utilisé.

La deuxième forme est :

Sélectionnez

#include <nom-de-fichier>

Ici, le nom est incomplet ; il ne mentionne pas le chemin d'accès au fichier, car il est convenu qu'il s'agit d'un fichier appartenant au système, faisant partie de la bibliothèque standard : le préprocesseur « sait » dans quels répertoires ces fichiers sont rangés. Ce sont des fichiers dont le nom se termine par .h (pour header, en-tête) et qui contiennent les déclarations nécessaires pour qu'un programme puisse utiliser correctement les fonctions de la bibliothèque standard.

Exemples (avec des noms de fichier en syntaxe UNIX) :

Sélectionnez

#include <stdio.h>
#include "/users/henri/projet_grandiose/gadgets.h"

Attention. Il ne faut pas confondre inclusion de fichiers et compilation séparée. Les fichiers que l'on inclut au moyen de la directive #include contiennent du texte source C qui sera compilé chaque fois que l'inclusion sera faite (c'est pourquoi ces fichiers ne contiennent jamais de fonctions). Le service rendu par la directive #include est de permettre d'avoir un texte en un seul exemplaire, non de permettre de le compiler une seule fois.

61Notez bien qu'il ne s'agit pas de fournir les fonctions de la bibliothèque (cela est fait par l'apport du fichier objet au moment de l'édition de liens) mais uniquement de donner l'information requise pour que les appels de ces fonctions puissent être correctement traduits par le compilateur. Pour cette raison on ne trouve jamais dans les fichiers en-tête ni des variables non externes, ni le corps des fonctions.

VIII-A-2. Définition et appel des "macros"

Les macros sans argument 62 se définissent par la directive :

Sélectionnez

#define nom corps

Le nom de la macro est un identificateur. Le corps de la macro s'étend jusqu'à la fin de la ligne 63. Partout dans le texte ou le nom de la macro apparait en qualité d'identificateur (ce qui exclut les commentaires, les chaines de caractères et les occurrences ou le nom de la macro est collé à un autre identificateur), il sera remplacé par le corps de la macro, lequel est un texte quelconque n'ayant à obéir à aucune syntaxe. Par exemple, entre l'endroit ou figurent les deux directives suivantes :

Sélectionnez

#define NBLIGNES 15
#define NBCOLONNES (2 * NBLIGNES)

et la fin du fichier où elles apparaissent, chaque occurrence de l'identificateur NBLIGNES sera remplacée par le texte 15 et chaque occurrence de l'identificateur NBCOLONNES par le texte (2 * 15). Au moins d'un point de vue logique, ces remplacements se feront avant que la compilation ne commence. Ainsi, à la place de

Sélectionnez

double matrice[NBLIGNES][NBCOLONNES];

le compilateur trouvera

Sélectionnez

double matrice[15][(2 * 15)];

(il remplacera immédiatement 2 * 15 par 30).

Attention. Voici une erreur que l'on peut faire :

Sélectionnez

#define NBLIGNES 15;
#define NBCOLONNES (2 * NBLIGNES);

C'est une erreur difficile à déceler, car le compilateur ne signalera ici rien de particulier. Mais plus loin il donnera pour erronée une déclaration comme :

Sélectionnez

double matrice[NBLIGNES][NBCOLONNES];

En effet, cette expression qui parait sans défaut aura été transformée par le préprocesseur en 64

Sélectionnez

double matrice[15;][(2 * 15;);];

Macros avec arguments. Elles se définissent par des expressions de la forme :

Sélectionnez

#define nom(ident1, ... identk) corps

Il ne doit pas y avoir de blanc entre le nom de la macro et la parenthèse ouvrante. Comme précédemment, le corps de la macro s'étend jusqu'à la fin de la ligne. Les identificateurs entre parenthèses sont les arguments de la macro ; ils apparaissent aussi dans le corps de celle-ci. Les utilisations (on dit les appels) de la macro se font sous la forme

Sélectionnez

nom(texte1, ... textek)

Cette expression sera remplacée par le corps de la macro, dans lequel ident1 aura été remplacé par texte1, ident2 par texte2, etc. Exemple 65 :

Sélectionnez

#define permuter(a, b, type) { type w_; w_ = a; a = b; b = w_; }

Exemple d'appel :

Sélectionnez

permuter(t[i], t[j], short *)

Résultat de la substitution (on dit aussi développement) de la macro :

Sélectionnez

{ short * w_; w_ = t[i]; t[i] = t[j]; t[j] = w_; }

Les appels de macros ressemblent beaucoup aux appels de fonctions. La même facilité, ne pas récrire un certain code source, peut être obtenue avec une macro et avec une fonction. Mais il faut comprendre que ce n'est pas du tout la même chose. Alors que le corps d'une fonction figure en un seul exemplaire dans le programme exécutable dont elle fait partie, le corps d'une macro est recopié puis compilé à chaque endroit ou figure l'appel de la macro.

Il faut donc que le corps d'une macro soit réduit, sans quoi son utilisation peut finir par être très onéreuse en termes d'espace occupé. Un autre inconvénient des macros est que leur définition « hors syntaxe » les rend très difficiles à maitriser au-delà d'un certain degré de complexité (pas de variables locales, etc.).

En faveur des macros : leur efficacité. Le texte de la macro étant recopié à la place de l'appel, on gagne à chaque fois le coût d'un appel de fonction. Il eût été bien peu efficace d'appeler une fonction pour ne faire que la permutation de deux variables ! Autre intérêt des macros, elles sont indépendantes des types de leurs arguments ou même, comme ci-dessus, elles peuvent avoir un type pour argument.

En définitive, la principale utilité des macros est d'améliorer l'expressivité et la portabilité des programmes en centralisant la définition d'opérations compliquées, proches du matériel ou sujettes à modification. Voici un exemple élémentaire : en C original (dans lequel on ne dispose pas du type void *) la fonction malloc doit être généralement utilisée en association avec un changement de type :

Sélectionnez

p = (machin *) malloc(sizeof(machin));

Des telles expressions peuvent figurer à de nombreux endroits d'un programme. Si nous définissons la macro

Sélectionnez

#define NOUVEAU(type) ((type *) malloc(sizeof(type)))

alors les allocations d'espace mémoire s'écriront de manière bien plus simple et expressive

Sélectionnez

p = NOUVEAU(machin);

De plus, un éventuel remplacement ultérieur de malloc par un autre procédé d'obtention de mémoire sera facile et fiable, puisqu'il n'y aura qu'un point du programme à modifier.

Attention

1. Il est quasiment obligatoire d'écrire les arguments et le corps des macros entre parenthèses, pour éviter des problèmes liés aux priorités des opérateurs. Par exemple, la macro

Sélectionnez

#define TVA(p) 0.196 * p

est très imprudente, car une expression comme (int) TVA(x) sera développée en (int) 0.196 * x, ce qui vaut toujours 0. Première correction :

Sélectionnez

#define TVA(p) (0.186 * p)

Le problème précédent est corrigé, mais il y en a un autre : l'expression TVA(p1 + p2) est développée en (0.186 * p1 + p2), ce qui est probablement erroné. D'où la forme habituelle :

Sélectionnez

#define TVA(p) (0.186 * (p))

2. Il ne faut jamais appeler une macro inconnue avec des arguments qui ont un effet de bord, car ceux-ci peuvent être évalués plus d'une fois. Exemple classique : on dispose d'une macro nommée toupper qui transforme toute lettre minuscule en la majuscule correspondante, et ne fait rien aux autres caractères. Elle pourrait être ainsi définie (mais on n'est pas censé le savoir) :

Sélectionnez

#define toupper(c) ('a' <= (c) && (c) <= 'z' ? (c) + ('A' - 'a') : (c))

On voit que l'évaluation de toupper(c) comporte au moins deux évaluations de c. Voici un très mauvais appel de cette macro :

Sélectionnez

calu = toupper(getchar())

En effet, cette expression se développe en

Sélectionnez

calu = ('a' <= (getchar()) && (getchar()) <= 'z'
? (getchar()) + ('A' - 'a') : (getchar()));

A chaque appel, au moins deux caractères sont lus, ce qui n'est sûrement pas l'effet recherché. On notera que si toupper avait été une fonction, l'expression toupper(getchar()) aurait été tout à fait correcte. Mais puisque ce n'en est pas une, il faut l'appeler avec un argument sans effet de bord. Par exemple :

Sélectionnez

{ calu = getchar(); calu = toupper(calu); }

62Le mot macro est une abréviation de l'expression « macro-instruction » désignant un mécanisme qui existe depuis longtemps dans beaucoup d'assembleurs. Les macros avec arguments sont appelées aussi pseudo-fonctions.

63Mais il découle de la section 1.2.1 que le corps d'une macro peut occuper en fait plusieurs lignes : il suffit que chacune d'elles, sauf la dernière, se termine par le caractère n.

64Pour permettre de trouver ce genre d'erreurs, certains compilateurs C possèdent une option (sous UNIX l'option -E) qui produit l'affichage du programme source uniquement transformé par le préprocesseur.

65Dans cet exemple, les accolades f et g donnent au corps de la macro une structure de bloc qui sera comprise et exploitée par le compilateur, mais cette structure est parfaitement indifférente au préprocesseur.

VIII-A-3. Compilation conditionnelle

Les directives de conditionnelle (compilation) se présentent sous les formes suivantes :

Image non disponible

La partie else est facultative ; on a donc droit aussi aux formes :

Image non disponible

Lorsque le premier élément de cette construction est #if expression, expression doit pouvoir être évaluée par le préprocesseur. Ce doit donc être une expression constante ne contenant pas l'opérateur de changement de type ni les opérateurs sizeof ou &. Elle sera évaluée et

  • si elle est vraie, c'est-à-dire non nulle, alors le texte qui se trouve entre #if et #else sera traité par le compilateur, tandis que celui qui figure entre #else et #endif sera purement et simplement tenu pour inexistant ;
  • si elle est fausse, c'est-à-dire nulle, c'est le premier texte qui sera ignoré tandis que le second sera lu par le compilateur. Dans la forme sans #else, si la condition est fausse aucun texte ne sera compilé.

Les directives

Sélectionnez

#ifdef identificateur
#ifndef identificateur

équivalent respectivement à

Sélectionnez

#if « identificateur est le nom d'une macro actuellement définie »
#if « identificateur n'est pas le nom d'une macro actuellement définie »

La compilation conditionnelle se révèle être un outil précieux pour contrôler les parties des programmes qui dépendent de la machine ou du système sous-jacent. On obtient de la sorte des textes sources qui restent portables malgré le fait qu'ils contiennent des éléments non portables. Voici un exemple : les fonctions de lecture- écriture sur des fichiers binaires ne sont pas les mêmes dans la bibliothèque ANSI et dans la bibliothèque UNIX classique. Une manière d'écrire un programme indépendant de ce fait consiste à enfermer les appels de ces fonctions et les déclarations qui s'y rapportent dans des séquences conditionnelles.

Déclaration :

Sélectionnez

...
#ifdef BINAIRES_ANSI
    FILE *fic;
#else
    int fic;
#endif
...

Ouverture :

Sélectionnez

...
#ifdef BINAIRES_ANSI
    fic = fopen(nom, "r");
    ok = (fic != NULL);
#else
    fic = open(nom, 0);
    ok = (fic >= 0);
#endif
...

Lecture :

Sélectionnez

...
#ifndef BINAIRES_ANSI
    ok = (fread(&art, sizeof(art), 1, fic) == 1);
#else
    ok = (read(fic, &art, sizeof(art)) == sizeof(art));
#endif
...

L'emploi de la compilation conditionnelle associée à la définition de noms de macros est rendu encore plus commode par le fait qu'on peut utiliser

  • des noms de macros normalement définis (#define...) ;
  • des noms de macros définis dans la commande qui lance la compilation, ce qui permet de changer le texte qui sera compilé sans toucher au fichier source. De plus, puisque ces définitions se font au niveau de la commande de compilation, elles pourront être faites par des procédures de commandes (scripts) donc automatisées ;
  • un ensemble de noms de macros prédéfinis dans chaque système, pour le caractériser. Par exemple dans le compilateur des systèmes UNIX la macro UNIX est définie et vaut 1.

Remarque. Le préprocesseur du C ANSI offre également l'opérateur defined qui permet d'écrire les directives #ifdef ident (resp. #ifndef ident) sous la forme #if defined ident (resp. #if !defined ident).

D'autre part, la construction

Sélectionnez

#if expression1
...
#else
#if expression2
...
#endif
#endif

peut en C ANSI s'abréger en

Sélectionnez

#if expression1
...
#elif expression2
...
#endif

VIII-B. La modularité de C

A ce point de notre exposé nous pouvons nous poser une question importante : le langage C est-il modulaire ? La modularité est une qualité que les bonnes méthodes de conception doivent posséder. Elle n'est pas facile à définir ; on peut toutefois la cerner à travers quelques critères 66.

Une méthode de conception doit

  • aider à diviser chaque nouveau problème en sous-problèmes qu'on peut résoudre séparément (critère de décomposabilité) ;
  • favoriser la production de composants logiciels qui peuvent se combiner librement pour produire de nouveaux systèmes (critère de composabilité) ;
  • permettre au concepteur d'écrire des modules dont chacun peut être compris isolément par un lecteur humain (critère de compréhensibilité) ;
  • permettre qu'une petite modification des spécifications du problème entraine uniquement la modification d'un petit nombre des modules de la solution (critère de continuité) ;
  • assurer que l'effet d'une condition anormale se produisant dans un module restera localisé à ce module ou, au pire, n'atteindra qu'un petit nombre de modules « voisins » ( critère de protection).

Théoriquement, tout langage de programmation peut servir à mettre en ¾uvre une méthode de conception modulaire, mais dans certains cas il faut une telle dose de ruse et d'autodiscipline, pour un résultat si peu fiable, que le jeu n'en vaut pas la chandelle. En pratique un langage est dit modulaire lorsqu'on peut, sans douleur et sans artifice, en faire l'outil d'une méthode de conception modulaire. L'expérience montre que les critères précédents induisent sur le langage en question quelques contraintes assez précises, comme celles-ci :

  • les modules doivent correspondre à des entités syntaxiques du langage ;
  • chaque module doit partager des informations avec aussi peu d'autres modules que possible, et quand un tel partage existe il doit concerner aussi peu d'éléments que possible ;
  • quand deux modules partagent des informations, cela doit être clairement indiqué dans leurs deux textes.

A la lumière de ces principes la modularité de C apparait fort rudimentaire, pour ne pas dire inexistante. Si l'on prend pour modules les fichiers sources qui composent un programme, on constate qu'aucune structure syntaxique ne signale les modules ni n'en délimite la portée, d'autant plus que le caractère non syntaxique de la directive #include brouille l'organisation du programme en fichiers distincts. Aucune déclaration particulière n'est requise pour indiquer que des objets sont partagés entre plusieurs modules. Chaque module communique avec tous les autres et, sauf spécification contraire, tous les objets de chaque module sont partagés.

Bien sûr, la compilation séparée est une des idées-clés du langage, et il est possible de rendre inaccessibles les noms qui peuvent être privés. Mais le langage offre peu d'outils pour rendre fiable le partage des noms qui doivent être publics, et ces outils restent d'un emploi facultatif, subordonné à l'autodiscipline du programmeur.

Par exemple, si dans un module B on doit référencer une variable ou une fonction définie dans un module A, il suffit d'écrire dans B une déclaration comme extern int x ;. Cet énoncé postule l'existence d'un objet nommé x, ce qui sera contrôlé par l'éditeur de liens. Mais il donne à x des attributs (la classe variable, le type int) que l'objet désigné par x ne possède pas forcément ; aucune vérification cependant ne sera faite. Ainsi la compilation de B se fait dans la plus totale insécurité.

66Suivant l'analyse de Bertrand Meyer (Conception et programmation par objets, InterEditions, 1990) à laquelle il semble difficile d'ajouter ou de retrancher quelque chose.

VIII-B-1. Fichiers en-tête

Le seul moyen dont dispose l'auteur d'un module A pour s'assurer que les autres modules qui forment un programme utilisent correctement les variables et fonctions qu'il rend publiques consiste à écrire un fichier entête (fichier A.h) contenant toutes les déclarations publiques. Ce fichier doit être inclus par la directive #include dans le module qui implante les objets publics (fichier A.c) et dans chacun des modules qui les utilisent. De cette manière tous ces fichiers « voient » les mêmes définitions de types, les mêmes déclarations de variables et les mêmes prototypes de fonctions ; ces déclarations sont écrites en un seul endroit, et toute modification de l'une d'entre elles se répercute sur tous les fichiers qui en dépendent.

La nécessité de ces fichiers en-tête apparait encore plus grande quand on considère le cas des bibliothèques, c'est-à-dire des modules que leurs fonctionnalités placent en position de prestataires de services vis-à-vis des autres modules qui composent un programme ; on parle alors de module serveur et de modules clients. En fait, on peut presque toujours voir la modularité en termes de serveurs et clients, car il y a toujours une hiérarchie parmi les modules. Le propre des bibliothèques est d'être conçues de manière indépendante des clients, afin de pouvoir être utilisées dans un programme présent et un nombre quelconque de programmes futurs. L'intérêt de leur associer le meilleur dispositif pour minimiser le risque de mauvaise utilisation est évident.

Typiquement, un fichier en-tête comportera les éléments suivants :

  • Des directives #include concernant les autres fichiers en-tête nécessaires pour la compréhension (par le compilateur) des éléments qui apparaissent dans le fichier en-tête en question.
  • Des définitions de constantes, soit sous forme de directives #define soit sous forme de type énuméré, qui sont des informations symboliques échangées entre le serveur et ses clients. Exemple : dans une bibliothèque graphique, les noms conventionnels des couleurs.
  • Des définitions de structures (struct, union) et de types (typedef) qui définissent la nature des objets manipulés par la bibliothèque. Typiquement, ces types permettent aux clients de déclarer les objets qui sont les arguments et les résultats des fonctions de la bibliothèque. Exemple : dans une bibliothèque graphique, la définition de types point, segment, etc.
  • Les déclarations extern des variables publiques du serveur. Les définitions correspondantes (sans le qualifieur extern) figureront dans le module serveur.

    Remarque. L'emploi de variables publiques est déconseillé ; un module ne devrait offrir que des fonctions 67.

  • Les déclarations des fonctions publiques du serveur. Les définitions correspondantes figureront dans le module serveur. En syntaxe originale seules les déclarations des fonctions qui ne rendent pas un entier sont nécessaires, mais même dans ce cas c'est une bonne habitude que d'y mettre les déclarations de toutes les fonctions, cela constitue un germe de documentation.

Bien entendu, tous les noms de variables et fonctions du module serveur qui ne figurent pas dans le fichier en-tête doivent être rendus privés (en les qualifiant static).

67S'il est utile qu'un client puisse consulter ou modifier une variable du serveur, écrivez une fonction qui ne fait que cela (mais en contrôlant la validité de la consultation ou de la modification). C'est bien plus sûr que de donner libre accès à la variable.

VIII-B-2. Exemple : stdio.h

A titre d'exemple visitons rapidement le plus utilisé des fichiers en-tête de la bibliothèque standard : stdio.h.

Notre but n'est pas de prolonger ici l'étude des entrées-sorties, mais d'illustrer les indications du paragraphe précédent sur l'écriture des fichiers en-tête. Le texte ci-après est fait de morceaux de la version MPW (Macintosh Programmer Workshop) du fichier stdio.h auquel nous avons enlevé ce qui ne sert pas notre propos :

Sélectionnez

/* stdIO.h -- Standard C I/O Package
 * Modified for use with Macintosh C
 * Apple Computer, Inc. 1985-1988
 *
 * Copyright American Telephone & Telegraph
 * Used with permission, Apple Computer Inc. (1985)
 * All rights reserved.
 */

#ifndef __STDIO__                            /* Voir le renvoi 1 ci-dessous */
#define __STDIO__

#include <stddef.h>
#include <stdarg.h>

/*
 * Miscellaneous constants
 */
#define EOF (-1)
#define BUFSIZ 1024
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
/*
 * The basic data structure for a stream is the FILE.
 */
typedef struct {                              /* Voir le renvoi 2 ci-dessous */
    int _cnt;
    unsigned char *_ptr;
    unsigned char *_base;
    unsigned char *_end;
    unsigned short _size;
    unsigned short _flag;
    unsigned short _file;
} FILE;
/*
 * Things used internally:
 */
extern FILE _iob[];                             /* Voir le renvoi 3 ci-dessous */
int _filbuf (FILE *);
int _flsbuf (int, FILE *);

/*
 * The standard predefined streams
 */
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
/*
 * File access functions
 */
int fclose (FILE *);                             /* Voir le renvoi 4 ci-dessous */
int fflush (FILE *);
FILE *fopen (const char *, const char *);
int setvbuf (FILE *, char *, int, size_t);
/*
 * Formatted input/output functions
 */
int printf (const char *, ...);                  /* Voir le renvoi 5 ci-dessous */
int fprintf (FILE *, const char *, ...);
int sprintf (char *, const char *, ...);
int scanf (const char *, ...);
int fscanf (FILE *, const char *, ...);
int sscanf (const char *, const char *, ...);

/*
 * Character input/output functions and macros
 */
int fgetc (FILE *);
int ungetc (int, FILE *);
int fputc (int, FILE *);
char *fgets (char *, int, FILE *);
char *gets (char *);
int puts (const char *);
int fputs (const char *, FILE *);
#define getc(p) (--(p)->_cnt >= 0 \
    ? (int) *(p)->_ptr++ : _filbuf(p))
#define getchar() getc(stdin)
#define putc(x, p) (--(p)->_cnt >= 0 \
    ? ((int) (*(p)->_ptr++ = (unsigned char) (x))) \
    : _flsbuf((unsigned char) (x), (p)))
#define putchar(x) putc((x), stdout)

/*
 * Direct input/output functions
 */
size_t fread (void *, size_t, size_t, FILE *);
size_t fwrite (const void *, size_t, size_t, FILE *);
#endif __STDIO__

Renvois :

1. Lorsque les fichiers en-tête sont d'un intérêt général ils finissent par être inclus dans de nombreux autres fichiers, eux-mêmes inclus les uns dans les autres. Le compilateur risque alors de lire plusieurs fois un même fichier, ce qui entraine un travail inutile et parfois des erreurs liées à des redéfinitions. Par exemple, imaginons que MonBazar.h soit un fichier comportant la directive #include <stdio.h>. Dans la compilation d'un fichier commençant par les deux directives

Sélectionnez

#include <stdio.h>
#include "MonBazar.h"

le compilateur risquerait de compiler deux fois le fichier stdio.h. Les deux premières directives de ce fichier résolvent ce problème : lorsque ce fichier est pris en compte une première fois, le nom __STDIO__ (nom improbable arbitrairement associé à stdio.h) devient défini. Durant la présente compilation, la directive #ifndef __STDIO__ fera que l'intérieur du fichier ne sera plus jamais lu par le compilateur. Tous les fichiers en-tête peuvent être protégés de cette manière contre les inclusions multiples.

2. On peut noter ici que la vue du type FILE qu'ont les modules clients est très différente de celle qu'ils auraient en utilisant un langage très modulaire comme Modula II ou Ada. Dans ces langages les noms et les types des champs d'une structure comme FILE resteraient privés, ainsi que la structure elle-même ; seul le type adresse d'une telle structure serait rendu public. Cela s'appelle un type opaque et augmente la fiabilité des programmes.

En C nous n'avons pas d'outils pour procéder de la sorte : ou bien un type est privé, connu uniquement du module serveur, ou bien il faut « tout dire » à son sujet. L'utilisation d'un pointeur générique, dans le style de

Sélectionnez

typedef void *FILE_ADDRESS;

dissimulerait bien l'information sur la structure FILE, mais aurait la conséquence de rendre impossibles à déceler les mauvaises utilisations du type en question par le client. Notez d'autre part que la connaissance dans les modules clients des noms des champs de la structure FILE est indispensable pour l'utilisation des macros comme getc ou putc.

3. Tous les noms qui apparaissent dans le fichier en-tête deviennent de ce fait publics, même ceux qu'on aurait aimé garder privés alors qu'on ne le peut pas, par exemple parce qu'ils apparaissent dans le corps d'une macro, comme ici le nom de la table des descripteurs de fichiers iob. Le langage C n'ayant rien prévu à ce effet, la « privacité » n'est assurée que par une convention entre le système et les utilisateurs : tout nom commençant par un blanc souligné « » appartient au système et l'utilisateur doit feindre d'en ignorer l'existence.

4. Ceci n'est pas la meilleure manière de donner les prototypes des fonctions dans un fichier en-tête. Dans une déclaration qui n'est pas une définition, la syntaxe n'exige pas les noms des arguments mais uniquement leurs types. Cependant, s'ils sont bien choisis, ces noms apportent une information supplémentaire qui augmente la sécurité d'utilisation de la bibliothèque. Par exemple, les prototypes

Sélectionnez

FILE *fopen(const char *, const char *);
size_t fread(void *, size_t, size_t, FILE *);

ne seraient d'aucune utilité à un programmeur qui hésiterait à propos du rôle ou de l'ordre des arguments de ces fonctions, contrairement à leurs versions « équivalentes » :

Sélectionnez

FILE *fopen(const char *filename, const char *mode);
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

5. Les appels de fonctions avec des arguments variables ne bénéficient pas des vérifications syntaxiques que le C ANSI effectue lorsque la fonction a fait l'objet d'une définition de prototype. Sauf pour les arguments nommés (le premier ou les deux premiers), les arguments que l'on passe à l'une de ces six fonctions échappent donc à tout contrôle du compilateur. L'information concernant la nature de ces arguments est portée par le format ; elle ne pourra être exploitée qu'à l'exécution.

Autres remarques :

Le premier client de ce dispositif doit être le fournisseur lui-même. Pour que la protection contre l'erreur recherchée soit effective, il faut que chacun des fichiers qui réalisent l'implantation des variables et fonctions « promises » dans stdio.h comporte la directive

Sélectionnez

#include <stdio.h>

De cette manière on garantit que l'auteur et l'utilisateur de chaque variable ou fonction sont bien d'accord sur la définition de l'entité en question.

Une autre règle à respecter par l'auteur du ou des modules serveurs : qualifier static tous les noms qui ne sont pas déclarés dans le fichier en-tête, pour éviter les collisions de noms. Le fichier en-tête joue ainsi, pour ce qui concerne les variables et les fonctions, le rôle de liste officielle des seuls noms publics.

VIII-C. Deux ou trois choses bien pratiques...

VIII-C-1. Les arguments du programme principal

Cette section ne concerne que les environnements, comme UNIX ou MS-DOS, dans lesquels les programmes sont activés en composant une commande de la forme nom-du-programme argument1 ... argumentk.

L'exécution d'un programme commence par la fonction main. Tout se passe comme si le système d'exploitation avait appelé cette fonction comme une fonction ordinaire. Il faut savoir que lors de cet appel, des arguments sont fournis au programme. Voici l'en-tête complet de main, en syntaxe ANSI :

Sélectionnez

int main(int argc, char *argv[])

avec :

  • argc : nombre d'arguments du programme
  • argv : tableau de chaines de caractères, qui sont les arguments du programme. Par convention, le premier argument est le nom du programme lui-même.

Imaginons avoir écrit un programme qui, une fois compilé, se nomme echo ; supposons que ce programme soit lancé par la commande :

Sélectionnez

echo Pierre Paul

alors, main reçoit les arguments que montre la figure 18.

Supposons que tout ce que l'on demande à echo soit de recopier la liste de ses arguments. Voici comment on pourrait écrire ce programme :

Fig. 18 - Arguments de main
Fig. 18 - Arguments de main
Sélectionnez

main(int argc, char *argv[]) {
int i;
for (i = 0; i < argc; i++)
printf("%s\n", argv[i]);
return 0;
}

La frappe de la commande « echo Pierre Paul » produit l'affichage de

Sélectionnez

echo
Pierre
Paul

La principale application de ce mécanisme est la fourniture à un programme des paramètres dont il peut dépendre, comme des noms de fichiers, des tailles de tableaux, etc. Par exemple, voici une nouvelle version du programme de copie de fichiers donné à la section 7.4.1, qui prend les noms des fichiers source et destination comme arguments :

Sélectionnez

#include <stdio.h>

#define PAS_D_ERREUR 			0
#define ERREUR_OUVERTURE 		1
#define ERREUR_CREATION 		2
#define PAS_ASSEZ_D_ARGUMENTS 		3

FILE *srce, *dest;

main(int argc, char *argv[]) {
    char tampon[512];
    int nombre;
    if (argc < 3)
        return PAS_ASSEZ_D_ARGUMENTS;
    if ((srce = fopen(argv[1], "rb")) == NULL)
        return ERREUR_OUVERTURE;
    if ((dest = fopen(argv[2], "wb")) == NULL)
        return ERREUR_CREATION;

    while ((nombre = fread(tampon, 1, 512, srce)) > 0)
        fwrite(tampon, 1, nombre, dest);

    fclose(dest);
    return PAS_D_ERREUR;
}

Si nous appelons copier le fichier exécutable produit en compilant le texte ci-dessus, alors nous pourrons l'exécuter en composant la commande

Sélectionnez

copier fichier-source fichier-destination

VIII-C-2. Branchements hors fonction : setter.h

Le mécanisme des longs branchements permet d'obtenir la terminaison immédiate de toutes les fonctions qui ont été appelées (et ne sont pas encore terminées) depuis que le contrôle est passé par un certain point du programme, quel que soit le nombre de ces fonctions. Il est réalisé à l'aide des deux fonctions :

Sélectionnez

int setter(jmp_buf contexte); void
longjmp(jmp_buf contexte, int code);

Cela fonctionne de la manière suivante : tout d'abord il faut déclarer une variable, généralement globale, de type jmp buf (type défini dans le fichier en-tête setter.h) :

Sélectionnez

#include <setter.h>
...
jmp_buf contexte;

L'appel

Sélectionnez

setter(contexte);

enregistre dans contexte certaines informations traduisant l'état du système, puis renvoie zéro. Ensuite, l'appel

Sélectionnez

longjmp(contexte, valeur);

remet le système dans l'état qui a été enregistré dans la variable contexte. Plus précisément, le système se trouve comme si l'appel de setter(contexte) venait tout juste de se terminer, en rendant cette fois non pas zéro mais la valeur indiquée dans l'appel de longjmp.

Le principal service rendu par ce dispositif est de permettre de programmer simplement, et en maitrisant le « point de chute », l'abandon d'une famille de fonctions qui se sont mutuellement appelées. Examinons un exemple classique : supposons qu'une certaine fonction expression soit un analyseur syntaxique présentant deux caractéristiques fréquentes :

  • la fonction expression est à l'origine d'une imbrication dynamique fort complexe (expression appelle une fonction terme, qui appelle une fonction facteur qui à son tour appelle une fonction primaire laquelle rappelle expression, etc.) ;
  • chacune des fonctions expression, terme, facteur, etc., peut à tout moment rencontrer une erreur dans ses données, qui rend la poursuite du traitement inutile ou impossible.
Fig. 19 - Branchement hors fonction
Fig. 19 - Branchement hors fonction

Voici comment l'appel d'expression pourrait être emballé dans une fonction « enveloppe » nommée analyse (voir la figure 19) :

Sélectionnez

#include <setter.h>
...
jmp_buf contexte;
...
int analyse(void) {
    if (setter(contexte) == 0) {
        expression();
        return SUCCES;
    }
    else
        return ERREUR;
}

Fonctionnement : lorsque la fonction analyse est activée, setter est appelée et rend 0 ; elle est donc immédiatement suivie par l'appel de expression. Si le travail de expression se termine normalement, alors analyse rendra SUCCES et elle aura été transparente. D'autre part, dans la fonction expression et toutes celles appelées « au-dessus » d'elle (terme, facteur, etc.), il est possible d'effectuer l'appel

Sélectionnez

longjmp(contexte, 1);

qui ramène le contrôle dans la fonction analyse, exactement sur la partie condition de l'instruction

Sélectionnez

if (setter(contexte) == 0)

mais cette fois la valeur rendue par setter sera 1 et analyse rendra donc la valeur ERREUR. Remarques :

  1. L'appel de longjmp se présente comme une remise du système dans l'un des états par lesquels il est passé. Cela est vrai, sauf pour ce qui concerne la valeur des variables, notamment les variables globales : elles ne reprennent pas la valeur qu'elles avaient lorsque longjmp a été appelée.
  2. On ne peut effectuer un appel de longjmp que pour ramener le système dans l'une des fonctions appelées et non encore terminées, pour laquelle l'espace local est encore alloué dans la pile. Appeler longjmp avec un contexte qui a été mémorisé dans une fonction dont l'activation est terminée est une erreur aux conséquences indéfinies, même si cette fonction a été rappelée depuis. Par exemple, un appel longjmp(c,v) qui serait placé dans la fonction main après l'appel analyse() (voir la figure 19) serait erroné, car il utiliserait le contexte d'une activation de fonction (la fonction analyse) qui n'existe plus.

VIII-C-3. Interruptions : signal.h

L'objet de la fonction signal est la détection d'événements asynchrones qui peuvent se produire pendant l'exécution d'un programme. « Asynchrones » signifie qu'on ne peut pas prévoir le moment ou ils se produiront, car ils ne résultent pas d'instructions normalement insérées dans la séquence qui forme le programme.

Un certain ensemble de types d'événements, dépendant de chaque système, est récupérable à travers le mécanisme décrit ici. En général on traite de la même manière le vrai et le faux asynchronisme. Une coupure de courant ou une interruption provoquée par l'utilisateur sont des événements vraiment imprévisibles. Une référence à travers un pointeur invalide ou une division par zéro ne sont pas réellement asynchrones (si on avait connu parfaitement le programme et les données on aurait pu prédire très exactement de tels événements), mais il est commode de les considérer comme tels et de les récupérer de la même manière.

La fonction signal est déclarée dans le fichier signal.h de la façon suivante :

Sélectionnez

void (*signal(int numero, void (*manip)(int)))(int);

Ce prototype n'est pas très facile à lire. Définissons le type PROCEDURE comme celui d'une fonction sans résultat défini ayant un argument de type int :

Sélectionnez

typedef void PROCEDURE(int);

Avec cela, la déclaration précédente se récrit plus simplement :

Sélectionnez

PROCEDURE *signal(int numero, PROCEDURE *manip);

et elle nous apprend que la fonction signal prend deux arguments, à savoir un int et l'adresse d'une PROCEDURE, et renvoie l'adresse d'une PROCEDURE.

Lors d'un appel de signal, l'argument numero doit désigner un des événements qu'il est possible de récupérer. L'effet de signal est d'enregistrer la PROCEDURE donnée en argument, de telle manière que si l'événement en question se produit, alors elle sera automatiquement appelée par le système avec pour argument le numéro de l'événement. La fonction signal rend l'adresse de la PROCEDURE qui était jusqu'alors enregistrée pour ce même événement.

Six événements sont prévus par le standard ANSI (ils ne sont pas tous implantés dans tous les systèmes ; en outre, chaque système peut ajouter ses propres particularités) :

  • SIGABRT : fin anormale du programme (appel de la fonction abort)
  • SIGINT : interruption provoquée par l'utilisateur (touche Ctrl-C, etc.)
  • SIGTERM : demande d'arrêt de l'utilisateur (interruption « forte »)
  • SIGFPE : erreur arithmétique (division par zéro, etc.)
  • SIGSEGV : accès mémoire illégal (souvent : mauvais pointeur dans l'accès à une donnée, débordement de tableau)
  • SIGILL : instruction illégale (souvent : mauvais pointeur dans l'appel d'une fonction).

La bibliothèque fournit en outre deux mécanismes de récupération d'une interruption prédéfinis :

  • SIG DFL : le mécanisme par défaut utilisé par le système pour l'événement en question lorsque la fonction signal n'a pas été appelée
  • SIG IGN : le mécanisme trivial qui consiste à ignorer l'événement

Exemple. Supposons qu'une section d'un certain programme effectue des calculs très complexes dont la durée risque d'inquiéter l'utilisateur final. On souhaite donc offrir à ce dernier la possibilité d'interrompre le programme lorsqu'il estime que celui-ci devient excessivement long, en appuyant sur une touche particulière 68. L'utilisateur apprendra alors l'état d'avancement de son calcul (traduit par la valeur de la variable iteration_numero) et aura à choisir entre la continuation et la terminaison du programme :

Sélectionnez

#include <signal.h>

int iteration_numero;
typedef void PROCEDURE(int);

void voirSkissPass(int numero) {
    int c;
    printf("J'en suis a l'iteration: %d\nOn arrete? ", iteration_numero);
    do
        c = getchar();
    while ((c = toupper(c)) != 'O' && c != 'N');
    if (c == 'O')
        exit(1); /* abandonner le programme */
    else
        return; /* reprendre le travail interrompu */
}

main() {
    PROCEDURE *maniprec;
    ...
    /* autres opérations */
    ...
/* entrée dans la section onéreuse */
    maniprec = signal(SIGINT, voirSkissPass);
    ...
    /* calculs terriblement complexes */
    ...
/* sortie de la section onéreuse */
    signal(SIGINT, maniprec);
    ...
    /* autres opérations */
    ...
}

68Sur plusieurs systèmes, comme UNIX et VMS, il s'agit de la combinaison des deux touches « Ctrl » et « C ».

VIII-D. La bibliothèque standard

La bibliothèque standard ANSI se compose de deux sortes d'éléments :

  • un ensemble de fichiers objets 69 contenant le code compilé des fonctions de la bibliothèque et participant, pour la plupart sans qu'il y soit nécessaire de l'indiquer explicitement, 70 à l'édition de liens de votre programme,
  • un ensemble de fichiers en-tête contenant les déclarations nécessaires pour que les appels de ces fonctions puissent être correctement compilés.

Ces fichiers en-tête sont organisés par thèmes ; nous reprenons cette organisation pour expliquer les principaux éléments de la bibliothèque.

Des parties importantes de la bibliothèque standard ont déjà été expliquées ; notamment :

  • la bibliothèque des entrées - sorties (associée au fichier stdio.h), à la section 7
  • les listes variables d'arguments (introduites dans le fichier stdarg.h) à la section 4.3.4
  • les branchements hors-fonction (déclarés dans le fichier setter.h) à la section 8.3.2
  • la récupération des interruptions (avec le fichier signal.h) à la section 8.3.3.

Nous passerons sous silence certains modules mineurs (comme la gestion de la date et l'heure). Au besoin, reportez-vous à la documentation de votre système.

69Il s'agit en réalité de fichiers « bibliothèques » (extensions .a, .lib, .dll, etc.), mais de tels fichiers ne sont que des fichiers objets un peu arrangés pour en faciliter l'emploi par les éditeurs de liens.

70Notez que, sous UNIX, lorsque des fichiers de la bibliothèque standard ne sont pas implicites, il y a un moyen simple de les indiquer explicitement. Par exemple, l'option -lm dans la commande gcc spécifie l'apport de la bibliothèque /lib/libm.a (ce fichier contient le code des fonctions mathématiques).

VIII-D-1. Aide à la mise au point : assert.h

Cette « bibliothèque » ne contient aucune fonction. Elle se compose d'une macro unique :

Sélectionnez

void assert(int expression)

qui fonctionne de la manière suivante : si l'expression indiquée est vraie (c'est-à-dire non nulle) au moment ou la macro est évaluée, il ne se passe rien. Si l'expression est fausse (c.-à-d. si elle vaut zéro), un message est imprimé sur stderr, de la forme

Sélectionnez

Assertion failed: expression, file fichier, line numero

ensuite l'exécution du programme est avortée. Exemple naïf :

Sélectionnez

#include <assert.h>
...
#define TAILLE 100
int table[TAILLE];
...
for (i = 0; j < TAILLE; i++) {
    ...
    assert(0 <= i && i < TAILLE);
    table[i] = 0;
    ...
}

l'exécution de ce programme donnera (puisqu'une faute de frappe a rendu infinie la boucle for ) :

Sélectionnez

Assertion failed: 0 <= i && i < TAILLE, file ex.c, line 241

La macro assert est un outil précieux pour la mise au point des programmes. Sans elle, il arrive souvent qu'une situation anormale produite en un point d'un programme ne se manifeste qu'en un autre point sans rapport avec avec le premier ; il est alors très difficile de remonter depuis la manifestation du défaut jusqu'à sa cause. Beaucoup de temps peut être gagné en postant de telles assertions aux endroits « délicats », ou les variables du programme doivent satisfaire des contraintes cruciales.

Il est clair, cependant, que cet outil s'adresse au programmeur et n'est utile que pendant le développement du programme. Les appels de assert ne doivent pas figurer dans l'exécutable livré à l'utilisateur final, pour deux raisons : d'une part, ce dernier n'a pas à connaitre des détails du programme source (en principe il ne connait même pas le langage de programmation employé), d'autre part parce que les appels de assert ralentissent les programmes.

Lorsque la mise au point d'un programme est terminée, on doit donc neutraliser tous les appels de assert qui y figurent. Cela peut s'obtenir en une seule opération, simplement en définissant en tête du programme 71 l'identificateur NDEBUG :

Sélectionnez

#define NDEBUG peu importe la valeur

juste avant d'effectuer la compilation finale du programme. Celle-ci se fera alors comme si tous les appels de assert avaient été gommés.

N.B. Le fait qu'il soit possible et utile de faire disparaitre les appels de assert du programme exécutable final montre bien que assert n'est pas la bonne manière de détecter des erreurs que le programmeur ne peut pas éviter, comme les erreurs dans les données saisies. Ainsi - sauf pour un programme éphémère à usage strictement personnel - le code suivant n'est pas acceptable :

Sélectionnez

...
scanf("%d", &x);
assert(0 <= x && x <= N); /* NON ! */
...

La vérification que 0 <= x && x <= N porte sur une valeur lue à l'exécution et garde tout son intérêt dans le programme final, lorsque les assert sont désarmés. C'est pourquoi elle doit plutôt être programmée avec du « vrai » code :

Sélectionnez

...
scanf("%d", &x);
if( ! (0 <= x && x <= N)) {
    fprintf(stderr, "Erreur. La valeur de x doit être comprise entre 0 et %d\n", N);
    exit(-1);
}
...

71Sous UNIX il n'est même pas nécessaire d'ajouter une ligne au programme : il suffit de composer la commande gcc en spécifiant une option « -D », comme ceci : gcc -DNEBUG -o monprog monprog.c etc.

VIII-D-2. Fonctions utilitaires : stdlib.h

• atoi

Sélectionnez

int atoi(const char *s), long atol(const char *s), double atof(const char *s)

Ces fonctions calculent et rendent l'int (resp. le long, le double) dont la chaine s est l'expression écrite.

Exemple :

Sélectionnez

int i;
char *s;
...
/* affectation de s */
...
i = atoi(s);
/* maintenant i a la valeur numérique dont la chaine s est l'expression textuelle */

Note. Pour faire le travail inverse de atoi ou une autre des fonctions précédentes on peut employer sprintf selon le schéma suivant :

Sélectionnez

int i;
char s[80];
...
/* affectation de i */
...
sprintf(s, "%d", i);
/* maintenant la chaine s est l'expression textuelle de la valeur de i */

• rand

Sélectionnez

int rand(void)

Le ieme appel de cette fonction rend le ieme terme d'une suite d'entiers pseudo-aléatoires compris entre 0 et la valeur de la constante RAND MAX, qui vaut au moins 32767. Cette suite ne dépend que de la valeur de la semence donnée lors de l'appel de srand, voir ci-dessous. Si srand n'a pas été appelée, la suite obtenue est celle qui correspond à srand(1). Voyez srand ci-après.

• srand

Sélectionnez

void srand(unsigned int semence)

Initialise une nouvelle suite pseudo-aléatoire, voir rand ci-dessus. Deux valeurs différentes de la semence { éloignées ou proches, peu importe { donnent lieu à des suites tout à fait différentes, du moins à partir du deuxième terme.

Application. Pour que la suite aléatoire fournie par les appels de rand que fait un programme soit différente à chaque exécution de ce dernier il fait donc commencer par faire un appel de srand en veillant à lui passer un argument différent chaque fois. Une manière d'obtenir cela consiste à utiliser l'heure courante (elle change tout le temps !), par exemple à travers la fonction time 72. Par exemple, le programme suivant initialise et affiche une suite pseudo-aléatoire de dix nombres flottants xi vérifiant 0 ≤ xi ≤ 1 :

Sélectionnez

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

main() {
    int i;
    float x;

    srand(time(NULL)); 		/* version C de randomize */
    rand(); 		/* le premier tirage est très lié à la semence */

    for(i = 0; i < 10; i++) {
        x = rand() / (float) RAND_MAX;
        printf("%f\n", x);
    }
}

• malloc

Sélectionnez

void *malloc(size t taille)

Alloue un espace pouvant mémoriser un objet ayant la taille indiquée, ou NULL en cas d'échec. Cet espace n'est pas initialisé.

• calloc

Sélectionnez

void *calloc(size t nombre, size t taille)

Alloue un espace suffisant pour loger un tableau de nombre objets, chacun ayant la taille indiquée, ou NULL en cas d'échec. L'espace alloué est initialisé par des zéros.

• free

Sélectionnez

void free(void *adresse)

Indique au système que l'espace mémoire ayant l'adresse indiquée n'est plus utile au programme. Cette adresse doit nécessairement provenir d'un appel de malloc, calloc ou realloc.

• realloc

Sélectionnez

void *realloc(void *adr, size t taille)

Ré-allocation d'espace. La valeur de adr doit provenir d'un appel de malloc, calloc ou realloc. Cette fonction essaie d'agrandir ou de rétrécir (usage rare) l'espace pointé par adr afin qu'il ait la la taille demandée. Si cela peut se faire sur place alors la fonction renvoie la même adresse adr. Sinon, elle obtient un nouvel espace ayant la taille voulue, y copie le contenu de l'espace pointé par adr, libère cet espace, enfin renvoie l'adresse de l'espace nouvellement alloué.

• exit

Sélectionnez

void exit(int code)

Produit l'arrêt normal (fermeture des fichiers ouverts, etc.) du programme. La valeur du code indiqué est transmise au système d'exploitation, cette valeur est utilisée lorsque le programme a été appelé dans le cadre d'un procédure de commandes (ou script). La signification de ce code dépend du système ; on peut utiliser les constantes :

  • EXIT SUCCESS : la valeur conventionnelle qui indique, pour le système sous-jacent, que le programme a réussi à accomplir sa mission
  • EXIT FAILURE : le programme n'a pas réussi

• abort

Sélectionnez

void abort(void)

Provoque un arrêt anormal du programme (c'est un événement détecté par la fonction signal).

• system

Sélectionnez

int system(const char *commande)

Suspend momentanément l'exécution du programme en cours, demande à l'interprète des commandes du système d'exploitation d'exécuter la commande indiquée et rend le code donné par cette commande (cette valeur dépend du système). La chaine commande doit être l'expression complète, avec options et arguments, d'une commande légale pour le système utilisé.

Toutes les versions de C n'offrent pas ce service, mais on peut savoir ce qu'il en est : l'appel system(NULL) rend une valeur non nulle si l'interprète de commandes peut effectivement être appelé depuis un programme, zéro sinon.

Exemple (bien connu des utilisateurs de Dev-C++ ; pause est une commande MS-DOS/Windows) :

Sélectionnez

system("pause");
char *getenv(char *nom)

Rend la chaine qui est la valeur de la variable d'environnement ayant le nom indiqué, ou NULL si une telle variable n'est pas définie. La notion de variable d'environnement est définie au niveau du système sous-jacent.

• qsort

Sélectionnez

void qsort(void *tab, size t nbr, size t taille, int (*comp)(const void *, const void *))

Quick sort, ou tri rapide. Cette fonction trie sur place (c'est-à-dire sans utiliser un tableaux auxiliaire), par ordre croissant, un tableau dont tab donne l'adresse de base, formé de nbr objets chacun ayant la taille indiquée.

Pour comparer les éléments du tableau, qsort utilise la fonction qui est la valeur de l'argument comp.

Cette fonction doit prendre les adresses de deux objets comme ceux dont le tableau est fait, et rendre une valeur négative, nulle ou positive selon que le premier objet est respectivement inférieur, égal ou supérieur au second.

Voir à la section 6.3.2, exemple 2, un modèle d'utilisation de cette fonction.

• bsearch

Sélectionnez

int bsearch(const void *ptr, const void *tab, size t nbr, size t taille,
int (*comp)(const void *, const void *))

Binary search, recherche dichotomique. Cette fonction recherche dans un tableau qui doit être trié, ayant l'adresse de base donnée par tab, formé de nbr objets chacun ayant la taille indiquée, un objet égal à celui ayant ptr pour adresse. Pour cela, elle utilise la fonction de comparaison donnée par comp, qui est définie comme pour qsort, voir ci-dessus.

• abs

Sélectionnez

int abs(int x), long labs(long x)

Valeur absolue, avec un argument int (resp. long) 73.

72La fonction time renvoie le nombre de secondes écoulées depuis le premier janvier 1970. Elle renvoie donc un nombre entier qui change toutes les secondes.

Si on doit écrire un programme devant initialiser plusieurs suites aléatoires distinctes en moins d'une seconde { ce qui est quand même rarissime { on aura un problème, qu'on peut régler en faisant intervenir d'autres fonctions de mesure du temps. Par exemple clock compte le temps (mais c'est un temps relatif au lancement du programme) en fractions de seconde, par exemple des millisecondes.

73La fonction correspondante pour les réels s'appelle fabs (cf. section VIII.D.5).

VIII-D-3. Traitement de chaines : string.h

• strcpy

Sélectionnez

char *strcpy(char *destin, const char *source)

Copie la chaine source à l'adresse destin. Rend destin.

• strcat

Sélectionnez

char *strcat(char *destin, const char *source)

Copie la chaine source à la suite de la chaine destin. Rend destin.

• strcmp

Sélectionnez

int strcmp(const char *a, const char *b)

Compare les chaines a et b pour l'ordre lexicographique (c'est-à-dire l'ordre par lequel on range les mots dans un dictionnaire) et rend une valeur négative, nulle ou positive selon que a est, respectivement, inférieur, égal ou supérieur à b.

• strlen

Sélectionnez

size t strlen(const char *s)

Rend le nombre de caractères de la chaine s. Il s'agit du nombre de caractères utiles : le caractère 'n0' qui se trouve à la fin de toutes les chaines n'est pas compté. Ainsi, strlen("ABC") vaut 3.

• memcpy

Sélectionnez

void *memcpy(char *destin, const char *source, size t nombre)

Copie la zone mémoire d'adresse source de de taille nombre dans la zone de même taille et d'adresse destin . Ces deux zones ne doivent pas se rencontrer.

• memmove

Sélectionnez

void *memmove(char *destin, const char *source, size t nombre)

Copie la zone mémoire d'adresse source de de taille nombre dans la zone de même taille et d'adresse destin . Fonctionne correctement même si ces deux zones se rencontrent ou se chevauchent.

VIII-D-4. Classification des caractères : ctype.h

Les éléments de cette bibliothèque peuvent être implantés soit par des fonctions, soit par des macros. Les prédicats rendent, lorsqu'ils sont vrais, une valeur non nulle qui n'est pas forcément égale à 1 :

  • int islower(int c) , c est une lettre minuscule.
  • int isupper(int c) , c est une lettre majuscule.
  • int isalpha(int c) , c est une lettre.
  • int isdigit(int c) , c est un chiffre décimal.
  • int isalnum(int c) , c est une lettre ou un chiffre.
  • int isspace(int c) , c est un caractère d'espacement : ' ', 'nt', 'nn', 'nr' ou 'nf'.
  • int iscntrl(int c) , c est un caractère de contrôle (c'est-à-dire un caractère dont le code ASCII est compris entre 0 et 31).
  • int isprint(int c) , c est un caractère imprimable, c'est-à-dire qu'il n'est pas un caractère de contrôle.
  • int isgraph(int c) , c est un caractère imprimable autre qu'un caractère d'espacement.
  • int ispunct(int c) , c est une ponctuation, c'est-à-dire un caractère imprimable qui n'est ni un caractère d'espacement, ni une lettre ni un chiffre.
  • int tolower(int c), int toupper(int c) si c est une lettre majuscule (resp. minuscule) rend la lettre minuscule (resp. majuscule) correspondante, sinon rend le caractère c lui-même.

VIII-D-5. Fonctions mathématiques : math.h

  • double sin(double x) rend la valeur de sin x
  • double cos(double x) rend la valeur de cos x
  • double tan(double x) rend la valeur de tan x
  • double asin(double x) rend la valeur de arcsin x, dans [-π/2,π/2]. On doit avoir x ∈ [-1,1]
  • double acos(double x) rend la valeur de arccos x, dans [0,π]. On doit avoir x ∈ [-1,1]
  • double atan(double x) rend la valeur de arctan x, dans [-π/2,π/2]
  • double atan2(double y, double x) rend la limite de la valeur de arctan v/u dans [-π/2,π/2] lorsque u → x+ et v → y. Pour x ≠ 0 c'est la même chose que arctan y/x
  • double sinh(double x) rend la valeur du sinus hyperbolique de x, soit sh x = (ex-e-x)/2
  • double cosh(double x) rend la valeur du cosinus hyperbolique de x, ch x = (ex+e-x)/2
  • double tanh(double x) rend la valeur de la tangente hyperbolique de x, th x = chx/shx
  • double exp(double x) rend la valeur de l'exponentielle de x, ex
  • double log(double x) rend la valeur du logarithme népérien de x, log x. On doit avoir x > 0
  • double log10(double x) rend la valeur du logarithme décimal de x, log10 x. On doit avoir x > 0
  • double pow(double x, double y) rend la valeur de xy. Il se produit une erreur si x = 0 et y = 0 ou si x < 0 et y n'est pas entier
  • double sqrt(double x) rend la valeur de √x. On doit avoir x ≥ 0
  • double ceil(double x) rend la valeur du plus petit entier supérieur ou égal à x, transformé en double
  • double floor(double x) rend la valeur du plus grand entier inférieur ou égal à x, transformé en double
  • double fabs(double x) la valeur absolue de x

VIII-D-6. Limites propres à l'implémentation : limits.h, float.h

Ces fichiers en-tête définissent les tailles et les domaines de variation pour les types numériques.

Le fichier limits.h définit :

  • CHAR_BIT nombre de bits par caractère.
  • CHAR_MAX, CHAR_MIN valeurs extrêmes d'un char.
  • SCHAR_MAX, SCHAR_MIN valeurs extrêmes d'un signed char.
  • UCHAR_MAX valeur maximum d'un unsigned char.
  • SHRT_MAX, SHRT_MIN valeurs extrêmes d'un short.
  • USHRT_MAX valeur maximum d'un unsigned short.
  • INT_MAX, INT_MIN valeurs extrêmes d'un int.
  • UINT_MAX valeur maximum d'un unsigned int.
  • LONG_MAX, LONG_MIN valeurs extrêmes d'un long.
  • ULONG_MAX valeur maximum d'un unsigned long.

Le fichier float.h définit :

  • FLT_DIG précision (nombre de chiffres décimaux de la mantisse).
  • FLT_EPSILON plus petit nombre e tel que 1.0 + e ≠ 1.0
  • FLT_MANT_DIG nombre de chiffres de la mantisse.
  • FLT_MAX plus grand nombre représentable.
  • FLT_MAX_10_EXP plus grand n tel que 10n soit représentable.
  • FLT_MAX_EXP plus grand exposant n tel que FLT_RADIXn - 1 soit représentable.
  • FLT_MIN plus petit nombre positif représentable (sous forme normalisée).
  • FLT_MIN_10_EXP plus petit exposant n tel que 10n soit représentable (sous forme normalisée).
  • FLT_MIN_EXP plus petit exposant n tel que FLT_RADIXn soit représentable (sous forme normalisée).
  • FLT_RADIX base de la représentation exponentielle.
  • FLT_ROUNDS type de l'arrondi pour l'addition.

Ces fichiers définissent également les constantes analogues pour les double (noms en « DBL_ ... » à la place de « FLT_ ... ») et les long double (noms en « LDBL_ ... »).


Programmation C/Version imprimable — Wikilivres

Programmation C/Version imprimable

Un livre de Wikilivres. Vu dans ... http://fr.wikibooks.org/wiki/Programmation_C/Version_imprimable
Programmation C

Ceci est la version imprimable de Programmation C.
  • Ce livre fait environ 100 pages au format A4 (dans la version du 15/06/2007).
  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.

Programmation C

Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
http://fr.wikibooks.org/wiki/Programmation_C

Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la Licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans Texte de dernière page de couverture. Une copie de cette licence est inclue dans l'annexe nommée « Licence de documentation libre GNU ».

Nous vous proposons, par ce livre, d'apprendre la programmation en langage C. Dans ce livre, nous supposerons que le lecteur a des notions de base en programmation, algorithmique et structures de données ; si vous n'en avez pas ou si vous voulez réviser des notions pendant l'apprentissage du C, vous pouvez consulter les Wikilivres associés : Programmation, Algorithmique et Structures de données.

Le langage C permet d'appréhender des principes informatiques de bas niveau, liés à l'architecture d'un ordinateur, comme la gestion de la mémoire. Si la connaissance de ces principes peut aider à comprendre certains côtés du langage C, ils ne sont cependant pas nécessaires pour lire ce Wikilivre, les concepts étant expliqués chaque fois que cela est nécessaire.

Ce livre, en constante évolution, est issu d'un projet collaboratif. Si vous trouvez des erreurs, vous pouvez le corriger directement à l'aide des liens « Modifier » sur chaque page. Vous pouvez poser des questions et apporter vos remarques en laissant un nouveau message sur la page de discussion de ce livre. Toute question ou participation est la bienvenue !

Bonne lecture !

[ modifier l'avant-propos ]

Note à l'attention des lecteurs de la version imprimable

Pour des raisons techniques, les notes de bas de pages sont rassemblées en fin d'ouvrage dans la version imprimable.

Voici une description rapide des différents chapitres qui constituent ce livre :

Introduction
Le C a une longue histoire... Créé il y a plus de 40 ans, il a beaucoup évolué depuis. Nous expliquerons ici son histoire, décrirons ses principales caractéristiques et domaines d'utilisation, et fournirons des raisons de l'apprendre (ou de ne pas l'apprendre).
Bases du langage
Dans cette partie, nous étudierons les concepts de base du langage C, sa syntaxe ainsi que la manière dont un programme est structuré.
Types de base
Ensuite, nous présenterons les différents types de base que fournit le langage C. Ces types étant pour l'essentiel des nombres (entiers, réels et complexes), ce chapitre traitera aussi de l'arithmétique associée à ces types. Les types caractères, qui sont la base de nombreux traitements, et ont un rôle particulier dans ce langage, seront aussi définis.
Classe de stockage
Le C permet de construire des constantes, par le biais de classes de stockages. Celles-ci permettent de définir plus généralement des types dérivés des types de base et seront étudiés dans ce chapitre.
Opérateurs
Nous étudierons ensuite les opérateurs permettant de travailler sur les types de base, ainsi que les règles générales qui structurent les expressions en C.
Structures de contrôle - tests
La notion d'expression définie, nous aborderons les moyens de contrôler le flux d'exécution d'un programme. Tout d'abord, les tests de type Si...Alors...Fin Si seront étudiés.
Structures de contrôle - itérations
Dans la suite du chapitre précédent, nous étudierons les structure d'itérations, ou boucles, de type Pour, Tant que ou dérivés.
Fonctions et procédures
Comment le C prend en charge les fonctions et procédures.
Tableaux
Comment le C gère les tables de données.
Pointeurs
Les pointeurs sont une spécificité du C, souvent mal comprise. Ce chapitre définira et montrera comment bien utiliser les pointeurs, ainsi que les erreurs à éviter dans leur manipulation.
Types avancés - structures, unions, énumérations
Explications sur les types avancés du C.
Préprocesseur
Le préprocesseur est un « langage » au-dessus du C, que l'utilisateur rencontre pour la première fois dans l'utilisation des directives d'inclusion d'en-têtes de la bibliothèque standard. Cependant, il est capable de bien plus, et fournit des outils comme les macros ou l'inclusion conditionnelle, qui sont l'objet de ce chapitre.
Bibliothèque standard
Cette partie a pour but de familiariser le lecteur avec la « bibliothèque standard » du C. Celle-ci propose un ensemble de fonctions, mais aussi de types ou de variables permettant d'effectuer des opérations spécifiques, comme les traitements de fichiers, les calculs mathématiques, ou la gestion des chaînes des caractères.
Chaînes de caractères
Ce chapitre traite de toutes les fonctions permettant d'effectuer des traitements sur les chaînes de caractères : concaténation, copie, recherche de caractères, etc.
Entrées/sorties
Ce chapitre traite des interactions avec l'utilisateur.
Erreurs
La bibliothèque standard utilise un mécanisme unique pour la gestion d'erreur, qui permet à un programme de déterminer précisément la cause de l'échec d'une fonction de la bibliothèque standard.
Mathématiques
Ce chapitre détaille les fonctions mathématiques fournies par la bibliothèque standard.
Gestion de la mémoire
La bibliothèque standard fournit un moyen d'allouer et de libérer dynamiquement de la mémoire au cours de l'exécution du programme. Cette fonctionnalité est surtout utilisée pour résoudre des problèmes dont le volume des données à traiter varie d'une exécution à l'autre.
Gestion des signaux
Un signal est une interruption logicielle reçue par un processus. Ces interruptions fournissent un moyen de communication entre le système et un programme, pour informer le programme de situations d'erreur (utilisation d'une zone de mémoire invalide, par exemple), ou informer un processus d'événements asynchrones (fin d'une opération d'entrée/sortie). Le C permet d'envoyer et de traiter la réception de ces signaux.
Conclusion

[ modifier le sommaire ]

Introduction

Historique

Naissance

Le langage C a été inventé aux Bells Labs en 1972 par Dennis Ritchie pour permettre l'écriture du système d'exploitation UNIX, alors développé par Ken Thompson et Dennis Ritchie.

Le système d'exploitation UNIX, né à la fin des années 1960 - début des années 1970, a été écrit directement en langage assembleur pour les machines auxquelles il était destiné. Si le langage assembleur permettait d'écrire un tel système, il n'en était pas moins peu aisé à utiliser. Un tel langage est en effet particulier à un type de processeur, ce qui fait que tout le système devait être réécrit pour le faire fonctionner sur une nouvelle architecture. Cela fait que son principal créateur, Ken Thompson, souhaita rapidement utiliser un langage plus évolué pour réécrire UNIX.

Parmi les langages disponibles à l'époque, BCPL (pour Basic Combined Programming Language, qui est une simplification de CPL), créé par Martin Richards en 1966, était intéressant. Sans entrer dans des descriptions détaillées, BCPL est un langage simple, procédural, et non typé. Sa simplicité permettait de créer facilement des compilateurs BCPL sur les machines de l'époque, où les ressources étaient très limitées (le premier ordinateur utilisé par Ken Thompson pour lancer Unix était un PDP-7, qui disposait d'une mémoire de 4000 mots de 18 bits, soit moins de 9 Ko). Ken Thompson l'a fait évoluer pour concevoir le langage B, qu'il a implémenté sur les premières machines UNIX. Cependant, certaines limitations du langage B ont fait qu'UNIX n'a pu être réécrit dans ce langage.

À partir de 1971, Dennis Ritchie fit évoluer B, pour répondre à ces problèmes. À l'image des programmeurs qui incrémentent les versions de leurs programmes, Ritchie « incrémenta » la lettre B pour appeler le nouveau langage C. Cette évolution se « stabilisa » vers 1973, année à partir de laquelle UNIX et les utilitaires systèmes d'UNIX ont été réécrits avec succès en C.

Développement

Par la suite en 1978, Brian W. Kernighan documenta très activement le langage, pour finalement publier avec Ritchie le livre de référence The C Programming Language. On appelle souvent C K&R le langage tel que spécifié dans la première édition de ce livre.

Dans les années qui suivirent, le langage C fut porté sur de nombreuses autres machines. Ces portages ont souvent été faits, au début, à partir du compilateur pcc de Steve Johnson, mais par la suite des compilateurs originaux furent développés indépendamment. Durant ces années, chaque compilateur C fut écrit en suivant les spécifications du K&R, mais certains ajoutaient des extensions, comme des types de données ou des fonctions supplémentaires, ou interprétaient différemment certaines parties du livre (qui n'était pas forcément très précis). À cause de cela, il fut de moins en moins facile d'écrire des programmes en C qui puissent fonctionner tels quels sur un grand nombre d'architectures.

Normalisation

Pour résoudre ce problème, et ajouter au C des possibilités dont le besoin se faisait sentir (et déjà existantes dans certains compilateurs), en 1989, l'organisme national de normalisation des USA (ANSI) normalisa le C. Le nom exact de cette norme est ANSI X3.159-1989 Programming Language C, mais elle fut (et est toujours) connue sous les dénominations ANSI C ou C89. Puis l'ANSI soumit cette norme à l'Organisation internationale de normalisation (ou ISO), qui l'accepta telle quelle et la publia l'année suivante sous le numéro ISO/IEC 9899:1990 (le document étant connu sous le nom ISO C90, ou C90). Le besoin de normalisation de l'époque étant extrêmement pressant, pratiquement tous les éditeurs de compilateurs C de l'époque se sont mis à modifier leurs compilateurs pour supporter la norme (et même n'ont pas attendu la publication officielle pour lancer ces modifications), et de fait la quasi totalité des implémentations existant à ce jour comprennent ces normes. L'ISO publia dans les années qui suivirent deux ensembles de correctifs, et un amendement pour la gestion des caractères internationaux.

Étant donnée la grande diversité des implémentations existants au moment de l'élaboration de la norme, et le principe de négociation qui base les processus de normalisation, la norme C est un compromis. Son but était principalement double : d'une part assurer le plus possible la portabilité du code C, pour simplifier le portage de programmes C d'une implémentation à une autre (ce qui était souvent un cauchemar à réaliser), d'autre part donner une certaine liberté aux éditeurs pour proposer des extensions spécifiques. Pour ce faire, la norme a défini des niveaux de conformité pour les programmes C et les implémentations, allant du programme strictement conforme, que toute implémentation doit accepter, et qui fonctionnera exactement de la même manière, aux programmes dépendant d'extensions.

On peut noter que le livre de Kernighan et Ritchie a été republié dans une seconde édition, pour refléter les changements du C89.

Enfin, en 1999, l'organisme ISO proposa une nouvelle version de la norme, qui reprenait quelques bonnes idées du langage C++ (voir plus bas). Il ajouta aussi le type long long d'une taille minimale de 64 bits, les types complexes, l'initialisation des structures avec des champs nommés, parmi les modifications les plus visibles. Le nouveau document, qui au niveau de l'ISO est celui ayant autorité aujourd'hui, est ISO/IEC 9899:1999, connu sous le sigle C99. Deux ensembles de correctifs à cette version ont été publiés jusqu'à présent. Au sens strict, la dénomination ISO C correspond donc actuellement à la norme de 1999 corrigée, mais l'usage est tel qu'il est préférable de toujours préciser de quelle version on parle (89/90 ou 99), et c'est ce qui sera fait dans cet ouvrage lorsque la distinction sera nécessaire. Il est à noter que, si la norme C90 a été largement adoptée par les éditeurs de compilateurs C, très peu implémentent la version C99. En effet, le besoin des utilisateurs s'est fait moins pressant, et l'effort nécessaire pour rendre les compilateurs conformes a rarement été mené à terme.

L'activité du groupe de travail de la norme C est, depuis, portée plus sur la « maintenance » du langage qu'à de véritables « évolutions ». La prochaine version, pour l'instant dénommée officieusement C1X et ISO/IEC 9899:201x officiellement, n'est pas encore publiée et ne sera pas étudiée dans ce livre[1].

La norme C spécifie la syntaxe du langage ainsi qu'une bibliothèque de fonctions simple. Cette bibliothèque est moins fournie que dans d'autres langages, mais cela est dû au domaine d'utilisation très varié de ce langage. En particulier, pour assurer une portabilité maximale du langage entre les implémentations embarquées, les ordinateurs de type PC et les supercalculateurs (par exemple), certaines fonctions n'ont pas été acceptées dans la norme. On peut citer, entre autres, la programmation parallèle, les communications entre processus, la communication réseau ou les interfaces graphiques...

Cela fait qu'en parallèle de cette normalisation du langage C, certaines extensions ont elles aussi été standardisées, voire normalisées. Ainsi, par exemple, des fonctions spécifiques aux systèmes UNIX, sur lesquels ce langage est toujours très populaire, et qui n'ont pas été intégrées dans la norme du langage C, ont servi à définir une partie de la norme POSIX.

Dans ce livre, nous étudierons surtout le langage C tel que défini dans les normes ISO citées ci-dessus. La norme C99 n'étant pas implémentée par tous les compilateurs, et beaucoup de programmes existants étant développés en C90, les différences entre les deux normes seront indiquées à chaque fois que nécessaire. Toutefois, nous illustrerons certains chapitres avec des exemples d'extensions courantes qu'un développeur C peut rencontrer, en particulier concernant les fonctions qui ne sont pas fournies par la norme C elle-même. Mais ces exemples d'extensions seront mineurs. D'autres Wikilivres étudient plus en détails de telles extensions, vous pouvez les trouver en regardant les livres listés sur la page Catégorie:C.

Norme et documents de référence

La norme qui définit le langage C est élaborée au niveau international par un groupe de travail (working group) de l'ISO. Ce groupe de travail fait partie plus précisément du comité technique commun à l'ISO et à la Commission Electrotechnique Internationale, le Joint Technical Commitee 1, qui élabore les normes internationales concernant les technologies de l'information. Il se décompose en sous-comités, donc le SC22, responsable des langages informatiques. Le SC22 rassemble plusieurs groupes de travail, chacun portant sur un langage spécifique, ou sur des considérations indépendantes des langages. On peut ainsi citer, par exemple :

  • JTC1/SC22/WG9 pour Ada ;
  • JTC1/SC22/WG14 pour le C ;
  • JTC1/SC22/WG21 pour le C++.

Comme pour toute norme internationale, c'est l'ISO qui décide des modalités de sa distribution. Comme la grande majorité des normes ISO, celle-ci n'est pas en libre distribution, mais en vente[2]. Le lecteur désirant acheter un exemplaire de la norme peut le faire sur le site officiel de l'ISO, par exemple.

Les corrections, ou questions, concernant la norme sont diffusées individuellement par le WG14 sur leur site sous le nom de Defect Report (DR), et rassemblées pour publication officielle dans des rectificatifs techniques ou Technical Corrigenda (TC). Deux ont été publiés pour le C99, en 2001 et 2004.

Par ailleurs, les brouillons (ou drafts) de la norme appartiennent au groupe de travail, qui peut décider de les publier. C'est le cas des documents n869, qui est le dernier brouillon disponible avant la publication de C99, et n1124, correspondant au C99 auquel ont été ajoutés les TC1 et TC2 (le TC3 ayant été publié en 2007).

On pourra noter que, tout comme l'ANSI l'a fait pour le C89, le WG14 a publié un Rationale, qui est un commentaire de la norme C99. Ces deux textes (en anglais, toujours) peuvent être intéressants pour comprendre les motivations de certains choix dans l'évolution du langage.

Tous ces documents sont disponibles sur le site officiel du groupe de travail (les URLs et références se trouvent dans la bibliographie à la fin de ce livre).

Toutefois, il faut bien noter qu'une norme est un texte très technique, qui sert de référence, et est très loin d'être un texte pédagogique. Un débutant fera mieux de lire un ouvrage comme ce wikilivre pour apprendre le C que lire directement la norme, au risque d'être rapidement découragé. Le présent ouvrage est construit de manière à ne pas avoir à recourir au texte de la norme, et n'y fera référence que rarement, mais le lecteur intéressé et averti pourra y vérifier des points complexes qui ne seront pas détaillés ici.

C et C++

Ce livre ne porte pas sur le C++, mais ces deux langages étant d'apparence très proches, il convient, pour éviter la confusion au lecteur désireux d'apprendre l'un ou l'autre, de préciser qu'ils sont très différents et que, si la connaissance de l'un peut aider à l'apprentissage de l'autre, ils ne doivent pas être confondus. L'idée du langage qui est devenu C++ a été lancée par Bjarne Stroustrup en 1979, dans un souci de faire évoluer le C de manière à le rendre plus « robuste ». L'apport le plus visible de cette évolution est que le C++ est orienté objet, alors que le C est procédural, mais ce n'est pas le seul.

Le langage C++ s'est créé à partir du C puis, rapidement, en parallèle à lui, et en 1998 est devenu une norme ISO au même titre que le C. Les groupes de travail respectifs du C et du C++ communiquent régulièrement dans le but d'assurer une certaine compatibilité entre les deux langages. De fait, un certain nombre d'idées issues des réflexions autour du C++ se sont vues intégrées dans la norme C99[3]. Cependant, la croyance populaire selon laquelle le C est un sous-ensemble du C++, ou qu'utiliser un compilateur C++ pour un programme C ne pose pas de problème, est fausse, et à plus d'un titre :

  • un code source strictement conforme du point de vue du C peut être invalide au sens du C++[4] ;
  • un code source qui est à la fois strictement conforme pour le C et le C++ peut avoir un comportement différent dans ces deux langages[5].

Durant l'apprentissage du C, le lecteur devra donc faire attention. Un certain nombre d'éditeurs, mettant à profit la grande proximité des deux langages, distribuent ensemble un compilateur C et un compilateur C++, en appelant parfois l'ensemble C/C++, ce qui entraîne une grande confusion chez les utilisateurs, qui ont parfois du mal à savoir quel est le langage dans lequel ils travaillent. L'utilisateur doit donc apprendre comment fonctionne son implémentation, pour déterminer quel(s) langage(s), norme(s) et extension(s) elle supporte, et comment le faire fonctionner dans l'un ou l'autre mode.

Une liste de compilateurs, avec les supports des normes, est référencée en Bibliographie.

Pourquoi apprendre le langage C ?

Avec son approche bas-niveau, le C permet d'obtenir des programmes très optimisés, pratiquement autant que s'ils avaient été écrits directement en assembleur. Avec un peu d'effort, il est même possible d'utiliser une approche orientée objet, au prix d'une certaine rigueur que le langage et les compilateurs, dans une certaine mesure, sont très loin d'imposer.

Les systèmes d'exploitation pour ordinateur de bureau les plus répandus actuellement sont Windows de Microsoft, Mac OS X d'Apple, et GNU/Linux. Ils sont tous trois écrits en langage C. Pourquoi ? Parce que les systèmes d'exploitation tournent directement au dessus du matériel de la machine. Il n'y a pas de couche plus basse pour gérer leurs requêtes. À l'origine, les systèmes d'exploitation étaient écrits en assembleur, ce qui les rendait rapides et performants. Toutefois, écrire un OS en assembleur est une tâche pénible et cela produit du code qui ne peut s'exécuter que sur une seule architecture processeur, comme l'Intel X86 ou l'AMD 64. Écrire un OS dans un langage de plus haut niveau, comme le langage C, permet au programmeur de porter son système d'exploitation sur une autre architecture sans avoir à tout réécrire.

Kenneth Thompson (à gauche) et Dennis Ritchie (à droite), les créateurs du langage C

Mais pourquoi utiliser le C et non Java, Basic ou Perl ? Principalement à cause de la gestion de la mémoire. À la différence de la plupart des autres langages de programmation, le langage C permet au programmeur de gérer la mémoire de la manière qu'il aurait choisie s'il avait utilisé l'assembleur. Les langages comme le Java et le Perl permettent au programmeur de ne pas avoir à se soucier de l'allocation de la mémoire et des pointeurs. C'est en général un point positif, car il est assez pénible et inutile de devoir se soucier de la gestion de la mémoire lorsqu'on écrit un programme de haut niveau comme un rapport sur les résultats trimestriels.

Cependant lorsqu'on parle d'écrire un programme de bas niveau comme la partie du système d'exploitation qui s'occupe d'envoyer la chaîne d'octets correspondant à notre rapport trimestriel depuis la mémoire de l'ordinateur vers le buffer de la carte réseau afin de l'envoyer vers une imprimante réseau, avoir un accès direct à la mémoire est fondamental — ce qui est impossible à faire dans un langage comme Java par exemple. Les compilateurs C produisent de plus très souvent un code rapide et performant.

Est-ce vraiment si merveilleux que le langage C soit un langage si répandu ? Par effet domino, la génération suivante de programmes suit la tendance de ses ancêtres. Les systèmes d'exploitation écrits en C ont toujours des librairies écrites en C. Ces bibliothèques système sont à leur tour utilisées pour écrire des bibliothèques de plus haut niveau (comme OpenGL ou GTK) et le programmeur de ces bibliothèques décide souvent d'utiliser le même langage que celui utilisé par ces bibliothèques système. Les développeurs d'applications utilisent ces bibliothèques de haut niveau pour écrire des traitements de texte, des jeux, les lecteurs multimédia, etc... La plupart d'entre eux choisiront d'utiliser pour leur programme le même langage que les bibliothèques de haut niveau. Et le schéma se reproduit à l'infini...

Bases du langage

Bonjour !

L'un des plus petits programmes possible en langage C est :

  1. #include <stdio.h>

  2.

  3. int main(void)

  4. {

  5.     printf("Bonjour !\n");

  6.     return 0;

  7. }

(Les numéros de lignes ont été ajoutés pour la lisibilité, mais ne font pas partie du programme.)

Ce programme a pour seul but d'afficher le texte « Bonjour ! » suivi d'un retour à la ligne. Voici la description de chaque ligne qui le compose :

  1. #include <stdio.h>

Inclusion de l'en-tête nommé <stdio.h>. Il est lié à la gestion des entrées et sorties (STanDard Input Output, entrées/sorties standards) et contient, entre autres, la déclaration de la fonction printf permettant d'afficher du texte formaté.

  3. int main(void)

Définition de la fonction principale du programme, main (principal en anglais). Il s'agit de la fonction appelée au démarrage du programme, tout programme en C doit donc la définir. La partie entre parenthèses spécifie les paramètres que reçoit le programme : ici, le programme n'utilisera aucun paramètre, ce qui est spécifié en C avec le mot-clé void (vide). La partie à gauche du nom de la fonction main spécifie le type renvoyé par la fonction : la fonction main est définie par la norme C comme renvoyant une valeur entière, de type int (pour integer, entier), au système d'exploitation.

  4. {

Début de la définition de la fonction main.

  5.     printf("Bonjour !\n");

Appel de la fonction printf (print formatted, affichage formaté, qui sera expliquée dans le chapitre dédié à cette fonction). La chaîne « Bonjour ! » va être affichée, elle sera suivie d'un retour à la ligne, représenté en C par \n.

  6.     return 0;

La fonction main a été définie comme retournant (to return) une valeur de type int, on renvoie donc une telle valeur. Par convention, la valeur 0 indique au système d'exploitation que le programme s'est terminé normalement.

  7. }

Fin de la définition de la fonction main.

Ce premier exemple ne détaille évidemment pas tout ce qui concerne la fonction main, ou printf, par exemple. La suite de l'ouvrage précisera les points nécessaires dans les chapitres appropriés (voir par exemple le paragraphe la fonction main du chapitre Fonctions et procédures, ou le paragraphe Sorties formatées du chapitre Entrées/sorties).

Compilation

Pour exécuter un programme C, il faut préalablement le compiler?.

Tapez le code source du programme Bonjour ! dans un éditeur de texte? et sauvegardez-le sous le nom bonjour.c (l'extension .c n'est pas obligatoire, mais est usuelle pour un fichier source en C). Ouvrez une fenêtre de commandes (terminal sous Unix, commandes MS-DOS sous Windows), et placez-vous dans le répertoire où est sauvegardé votre fichier. Pour compiler avec le compilateur standard de votre système, il faut taper la commande :

cc bonjour.c

D'autres compilateurs pour le langage C peuvent être utilisés. La compilation du programme produira un fichier exécutable, appelé a.out sous Unix, et a.exe sous Windows. Il peut être exécuté en tapant ./a.out sous Unix, et a sous Windows.

Éléments de syntaxe

Identificateurs

Les identificateurs commencent par une lettre ou le caractère souligné ("_") et peuvent contenir des lettres, des chiffres et le caractère souligné (cependant, les identificateurs commençant par deux caractères soulignés, ou un caractère souligné suivi d'une majuscule sont réservés par l'implémentation, et ne doivent pas être utilisés dans un programme « ordinaire »). Tous les mots-clés ainsi que les symboles (variables, fonctions, champs, etc.) sont sensibles à la casse des lettres. Quelques exemples d'identificateurs valides :

toto
_coin_coin76

Mots réservés du langage

Le langage, dans la norme C90, possède 32 mots réservés (ou mots-clés) qui ne peuvent pas être utilisés comme identificateurs. La norme C99 en a ajouté cinq ; la norme C11 en a de nouveau apporté 7. Le tableau ci-dessous les liste tous, avec la mention « C99 » ou « C11 » pour indiquer ceux définis uniquement dans les versions correspondantes ou ultérieures :

auto | break | case | char | const | continue | default | do
double | else | enum | extern | float | for | goto | if
inline (C99) | int | long | register | restrict (C99) | return | short | signed
sizeof | static | struct | switch | typedef | union | unsigned | void
volatile | while | _Alignas (C11) | _Alignof (C11) | _Atomic (C11) | _Bool (C99) | _Complex (C99) | _Generic (C11)
_Imaginary (C99) | _Noreturn (C11) | _Static_assert (C11) | _Thread_local (C11)

Un programme C90 peut par exemple définir inline comme un identifiant de variable ou de fonction mais cela est très déconseillé, car il sera invalide au sens de la norme C99. Une mise à jour du compilateur peut alors l'empêcher de compiler le code source en question. Pour éviter de telles situations, il est préférable de considérer tous les mots-clés du tableau précédent comme étant réservés, quels que soient le compilateur et la norme utilisés.

Commentaires

Les commentaires commencent par /* et se terminent par */, ils ne peuvent pas être imbriquées :

/* ceci est un commentaire */

/*

 ceci est aussi un commentaire

 */

/* /* ceci est encore un commentaire */
Avertissement Ce code contient une erreur volontaire !
/* /* */ ceci n’est pas un commentaire */

Inclure des commentaires pertinents dans un programme est un art subtil. Cela nécessite un peu de pratique pour savoir guider les lecteurs et attirer leur attention sur certaines parties délicates du code. Pour faire court, on ne saurait trop que rappeler ce célèbre vers de Nicolas Boileau, qui disait que « ce que l'on conçoit bien, s'énonce clairement et les mots pour le dire arrivent aisément » (Art Poétique, 1674). Ou encore, pour adopter une approche plus pragmatique : si un programmeur a du mal à commenter (expliquer, spécifier) le fonctionnement d'un passage de code, c'est que le code est mal conçu et qu'il serait bénéfique de le réécrire plus lisiblement.

Attention! Les commentaires ne devraient pas être employés pour désactiver certaines parties du code. Cette technique dissuade un programmeur d'en inclure puisque le langage C ne permet pas d'imbriquer les commentaires. Pour cela, le préprocesseur dispose d'instructions dédiées, qui permettent heureusement de désactiver du code en laissant les commentaires.

Le // ajoute la possibilité de placer un commentaire d'une seule ligne, à partir du // jusqu'à la fin de la ligne :

// commentaire
instruction; // autre commentaire

Cependant, ces commentaires « à la C++ » sont apparus dans C99, et ne sont pas permis en C90.

Instructions

Les instructions se terminent par un point-virgule (;), on peut placer autant d'instructions que l'on veut sur une même ligne (même si ce n'est pas conseillé pour la lisibilité du code). Les blocs d'instructions commencent par une accolade ouvrante ({) et se terminent par une accolade fermante (}). Les instructions doivent obligatoirement être déclarées dans une fonction : il est impossible d'appeler une fonction pour initialiser une variable globale par exemple (contrairement au C++).

/* une instruction */
i = 1;

/* plusieurs instructions sur la même ligne */
i = 1; j = 2; printf("bonjour\n");

/* un bloc */
{
        int i;
        i = 5;
}

/* l'instruction vide */
;

Dans la mesure où le compilateur ne se soucie pas des blancs (espaces et retours à la ligne), vous pouvez formater votre code comme vous l'entendez. Il y a beaucoup de religions concernant les styles d'indentation, mais pour faire court et éviter les guerres saintes, on ne saurait trop conseiller que d'utiliser le même nombre de blancs par niveau d'imbrication de bloc.

À noter que l'instruction vide étant valide en C, on peut donc pratiquement rajouter autant de point-virgules que l'on veut. Le point-virgule ne sert pas uniquement à marquer la fin des instructions, les compilateurs l'utilisent généralement comme caractère de synchronisation, suite à une erreur dans un programme source. En fait, en général, lorsqu'une erreur est détectée, les compilateurs ignorent tout jusqu'au prochain point-virgule. Ce qui peut avoir des conséquences assez dramatiques, comme dans l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
int traite_arguments(int nb, char * argv[])
{
         /* ... */
         return 0
}

int main(int nb, char * argv[])
{
         int retour;
         retour = traite_arguments(nb, argv);
         /* ... */
}

On notera l'absence de point-virgule à la fin de l'instruction return à la fin de la fonction traite_arguments. Ce que la plupart des compilateurs feront dans ce cas sera d'ignorer tout jusqu'au prochain point-virgule. On se rend compte du problème : on a sauté une déclaration de fonction (avec ses deux paramètres) et une déclaration de variable. Ce qui veut dire qu'une cascade d'erreurs va suivre suite à l'oubli... d'un seul caractère (;) !

Déclarations de variables

T var1, var2, ..., varN;

Cette ligne déclare les variables var1, var2, ..., varN de type T. Une variable est dite locale, si elle est définie à l'intérieur d'une fonction et globale si définie en-dehors.

Par exemple :

int jour; /* 'jour' est une variable globale, de type entier */

int main(void)
{
    double prix; /* 'prix' est une variable locale à la fonction 'main', de type réel */
    return 0;
}

Variables locales

Les variables locales (aussi appelées automatiques) ne sont visibles que dans le bloc dans lequel elles sont définies, et n'existent que durant ce bloc. Par exemple :

Avertissement Ce code contient une erreur volontaire !
int fonction(int n)
{
    int i; /* i est visible dans toute la fonction */
    i = n + 1;
    {  /* début d'un nouveau bloc */
        int j; /* j est visible dans le nouveau bloc *seulement* */
        j = 2 * i; /* i est accessible ici */
    }
    i = j; /* ERREUR : j n'est plus accessible */
    return i;
}

Le code précédent accède à i depuis un bloc contenu dans le bloc où i a été défini, ce qui est normal. Puis il essaye d'accéder à la valeur de la variable j en-dehors du bloc où elle à été définie, ce qui est une erreur.

Les variables locales ne sont pas initialisées automatiquement et contiennent donc, après leur déclaration, une valeur aléatoire. Utiliser une telle valeur peut causer un comportement aléatoire du programme. Le programme suivant essaye d'afficher la valeur d'une telle variable, et on ne peut savoir à l'avance ce qu'il va afficher. Si on le lance plusieurs fois, il peut afficher plusieurs fois la même valeur aussi bien qu'il peut afficher une valeur différente à chaque exécution :

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>

int main(void)
{
    int n;
    printf("La variable n vaut %d\n", n);
    return 0;
}

Avant la normalisation ISO C99, les déclarations de variables locales devaient obligatoirement être placées juste au début d'un bloc et s'arrêtaient à la première instruction rencontrée. Si une déclaration est faite au-delà, une erreur sera retournée. Suivant la norme C99, les déclarations peuvent se trouver n'importe où dans un bloc. Par exemple, le code suivant est correct suivant la norme C99, mais pas suivant la norme C90, car la variable a est définie après l'instruction puts("Bonjour !"); :

int ma_fonction(int n)
{
    puts("Bonjour !");
    int a;  /* Valide en C99, invalide en C90 */
    /* autre chose... */
    return a;
}

Variables globales

Une attention particulière doit être portée aux variables globales, souvent source de confusion et d'erreur. Utilisez des noms explicites, longs si besoin et limitez leur usage au seul fichier où elles sont déclarées, en les déclarants statiques.

Les variables globales sont initialisées avant que la fonction main s'exécute. Si le programmeur ne fournit pas de valeur initiale explicitement, chaque variable reçoit une valeur par défaut suivant son type :

  • un nombre (entier, réel ou complexe) est initialisé à 0 ;
  • les pointeurs sont initialisés à NULL ;
  • pour une structure, l'initialisation se fait récursivement, chaque membre étant initialisé suivant les mêmes règles ;
  • pour une union, le premier membre est initialisé suivant ces règles.

Pour initialiser explicitement une variable globale, on ne peut utiliser que des constantes ; en particulier, on ne peut appeler de fonction, contrairement à d'autres langages.

Avertissement Ce code contient une erreur volontaire !
int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour = jour_courant(); /* ERREUR ! */

int main(void)
{
   /* ... */
   return 0;
}

Le code précédent ne pourra pas compiler, car on ne peut appeler directement jour_courant() pour initialiser jour. Si vous avez besoin d'un tel comportement, une solution est de faire l’initialisation explicite dans main :

int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour; /* 'jour' est initialisé à 0 *avant* le début de 'main' */

int main(void)
{
   jour = jour_courant();
   /* ... */
   return 0;
}

Traitements des ambiguïtés

Le C utilise un mécanisme élégant pour lever des constructions syntaxiques en apparence ambiguës. L'analyse des mots (lexèmes, token en anglais) se fait systématiquement de la gauche vers la droite. Si plusieurs lexèmes peuvent correspondre à une certaine position, le plus grand aura priorité. Considérez l'expression valide suivante :

a+++b;

Ce genre de construction hautement illisible et absconse est bien évidemment à éviter, mais illustre parfaitement bien ce mécanisme. Dans cet exemple, le premier lexème trouvé est bien sûr l'identificateur 'a' puis le compilateur a le choix entre l'opérateur unaire '++', ou l'opérateur binaire '+'. Le plus grand étant le premier, c'est celui-ci qui aura priorité. L'instruction se décompose donc en :

(a ++) + b;

Types de base

Le C est un langage typé statiquement : chaque variable, chaque constante et chaque expression, a un type défini à la compilation. Le langage lui-même fournit des types permettant de manipuler des nombres (entiers, réels ou complexes) ou des caractères (eux-mêmes étant manipulés comme des entiers spéciaux), et permet de construire des types plus complexes à partir de ces premiers, par exemple en groupant des données de même type en tableaux, ou des données de types différents dans des structures. Dans ce chapitre, nous étudierons les types de base fournis par le C, l'étude des types complexes étant faite dans la suite du livre.

Entiers

Il y a cinq types de variables entières (« integer » en anglais) :

  • char ;
  • short int, ou plus simplement short ;
  • int ;
  • long int, ou long ;
  • long long int, ou long long (ce type a été ajouté depuis la norme C99).

Comme évoqué en introduction, le type caractère char est particulier, et sera étudié en détail plus bas.

Les types entiers peuvent prendre les modificateurs signed et unsigned qui permettent respectivement d'obtenir un type signé ou non signé. Ces modificateurs ne changent pas la taille des types. Le langage ne définit pas exactement leur tailles, mais définit un domaine de valeurs minimal pour chacun.

Représentation des entiers signés

La norme C tient compte des anciennes représentations des nombres signés telles que le signe+valeur absolue et le complément à 1. Ces deux représentations sont brièvement expliquées ci-dessous :

Signe + valeur absolue (SVA)
Un bit contient le signe du nombre (par ex : 0 pour +, 1 pour -), les autres bits sont utilisés pour la valeur absolue. On peut donc représenter 0 de deux manières : +0 (000...000) ou -0 (100..000). Sur N bits on peut donc représenter tout nombre entre -(2N-1-1) (111...111) et +(2N-1-1) (011...111).
Complément à 1 (CPL1)
Les bits des nombres négatifs sont inversés. On peut donc représenter 0 de deux manières : +0 (000...000) ou -0 (111..111). Sur N bits on peut donc représenter tout nombre entre -(2N-1-1) (100...000) et +(2N-1-1) (011...111).

Ces deux représentations peuvent représenter la valeur nulle de deux manières différentes.

La représentation moderne des nombres négatifs utilise le complément à 2 (CPL2) qui consiste à représenter les nombres négatifs comme le complément à 1 et en ajoutant 1. Sur un nombre fixe de bits, la valeur 0 n'a qu'une seule représentation : +0 (000...000) et -0 (111...111 + 1 = (1)000...000) ont deux représentations identiques. Sur N bits on peut donc représenter tout nombre entre -(2N-1) (100...000) et +(2N-1-1) (011...111). Cette représentation possède donc un domaine plus large.

Le tableau ci-dessous donne le domaine des valeurs quelle que soit la représentation utilisée (SVA, CPL1 ou CPL2) :

Domaines de valeurs minimaux des types entiers (C90 et C99) quelle que soit sa représentation (SVA, CPL1 ou CPL2)
Type | Taille | Borne inférieure | Borne inférieure (formule) | Borne supérieure | Borne supérieure (formule)
signed char | ≥ 8 bits | -128 | -(27) | +127 | 27-1
unsigned char | ≥ 8 bits | 0 | 0 | +255 | 28-1
short | ≥ 16 bits | -32 767 | -(215-1) | +32 767 | 215-1
unsigned short | ≥ 16 bits | 0 | 0 | +65 535 | 216-1
int | ≥ 16 bits | -32 767 | -(215-1) | +32 767 | 215-1
unsigned int | ≥ 16 bits | 0 | 0 | +65 535 | 216-1
long | ≥ 32 bits | -2 147 483 647 | -(231-1) | +2 147 483 647 | 231-1
unsigned long | ≥ 32 bits | 0 | 0 | +4 294 967 295 | 232-1
long long (C99) | ≥ 64 bits | -9 223 372 036 854 775 807 | -(263-1) | +9 223 372 036 854 775 807 | 263-1
unsigned long long (C99) | ≥ 64 bits | 0 | 0 | +18 446 744 073 709 551 615 | 264-1

Cette table signifie qu'un programme peut utiliser sans problème une variable de type int pour stocker la valeur 215-1, quel que soit le compilateur ou la machine sur laquelle va tourner le programme.

Par contre, une implémentation C peut fournir des domaines de valeurs plus larges que ceux indiqués au-dessus :

  • Les domaines indiqués pour les nombres signés dans le tableau précédent sont ceux d'une implémentation par complément à 1, ou par signe et valeur absolue. Pour le complément à 2, la borne inférieure est de la forme -2N-1, ce qui autorise une valeur supplémentaire (ex : int de -215 à +215-1, soit de -32 768 à +32 767),
  • un int implémenté sur 32 bits pourrait aller de -(231-1) à 231-1, par exemple[6], et un programme tournant sur une telle implémentation peut alors utiliser ces valeurs, mais il perdrait en portabilité.
Domaines de valeurs des types entiers représentés en complément à 2 (CPL2)
Type | Borne inférieure | Borne inférieure (formule) | Borne supérieure | Borne supérieure (formule)
signed char | -128 | -(27) | +127 | 27-1
unsigned char | 0 | 0 | +255 | 28-1
short | -32 768 | -(215) | +32 767 | 215-1
unsigned short | 0 | 0 | +65 535 | 216-1
int | -32 768 | -(215) | +32 767 | 215-1
unsigned int | 0 | 0 | +65 535 | 216-1
long | -2 147 483 648 | -(231) | +2 147 483 647 | 231-1
unsigned long | 0 | 0 | +4 294 967 295 | 232-1
long long (C99) | -9 223 372 036 854 775 808 | -(263) | +9 223 372 036 854 775 807 | 263-1
unsigned long long (C99) | 0 | 0 | +18 446 744 073 709 551 615 | 264-1

Par ailleurs, une relation d'ordre entre ces domaines de valeurs est garantie ; qui peut être exprimée ainsi :

domaine(char) ≤ domaine(short) ≤ domaine(int) ≤ domaine(long) ≤ domaine(long long)

Cela signifie que toutes les valeurs possibles pour une variable du type char sont aussi utilisables pour les autres types ; mais aussi que, par exemple, une valeur valide pour le type int peut ne pas être représentable dans une variable de type short.

Si vous ne savez pas quel type donner à une variable de type entier, le type int est par défaut le meilleur choix (à condition que votre donnée ne dépasse pas 215-1) : ce type est la plupart du temps représenté au niveau matériel par un « mot machine », c'est-à-dire qu'il est adapté à la taille que la machine peut traiter directement (il fait usuellement 32 bits sur un PC 32 bits, par exemple). Cela permet un traitement plus rapide par le matériel. De plus, beaucoup de bibliothèques (que ce soit celle fournie par le langage C ou d'autres) utilisent ce type pour passer des entiers, ce qui fait que l'utilisation de ces bibliothèques sera plus aisée.

Par ailleurs, un utilisateur peut connaître les domaines de valeurs exacts de sa machine en utilisant l'en-tête <limits.h>.

Portabilité apportée par C99

L'incertitude sur l'intervalle de valeur de chaque type en fonction de la machine peut s'avérer extrêmement gênante, pour ne pas dire rédhibitoire. En effet, certains programmes peuvent nécessiter un type de données de taille fixe et cependant être destinés à être portables. Pour ces programmes, les types entiers du C ne sont pas suffisants. Beaucoup d'extensions ont été rajoutées pour définir explicitement des types entiers à intervalle fixe (8, 16, 32 bits...) à partir des types de base, avec une nomenclature loin d'être homogène d'un compilateur à l'autre (ce qui, loin de résoudre le problème, ne faisait que le déplacer).

La norme ISO C99 décide une bonne fois pour toute de définir, dans l'en-tête <stdint.h>, plusieurs nouveaux types où N représente un nombre entier définissant la taille requise en bit :

  • des types implémentés optionnellement sur certaines architectures (à éviter ?) ;
    • entiers signés ou non et de longueur N exacte : uintN_t et intN_t ;
    • entiers pouvant contenir un pointeur : intptr_t et uintptr_t ;
  • des types requis par toutes les architectures respectant la norme C99 ;
    • entiers devant être plus grand que N bits au moins : int_leastN_t et uint_leastN_t ;
    • entiers rapides à calculer et plus grand que N bits au moins : int_fastN_t et uint_fastN_t ;
    • plus grand entier : intmax_t et uintmax_t.

Cet en-tête définit aussi des constantes pour les valeurs minimales et maximales de chaque type.

L'include <inttypes.h> définit les constantes symboliques à utiliser pour imprimer ces nouveaux types avec les fonctions de la famille de printf (PRIxxx) et les lire avec celles de scanf (SCNxxx).

Constantes numériques entières

Il existe différentes suites de caractères qui sont reconnues comme étant des constantes numériques entières :

  • un nombre en notation décimale : une suite de chiffres (0-9) ;
  • le caractère « 0 » suivi d'un nombre en notation octale : une suite de chiffres compris entre 0 et 7 ;
  • les caractères « 0x » suivi d'un nombre en notation hexadécimale : une suite de chiffres et des lettres a, b, c, d, e, f (ou A, B, C, D, E, F).

Par défaut, une constante numérique entière est de type int et, si sa valeur est trop grande pour le type int, elle prend celle du type « plus grand » suffisant. Comme les domaines de valeurs des types peuvent varier suivant la machine, le type effectif d'une constante peut lui aussi varier. Cela peut s'avérer problématique lors de passage de paramètres à des fonctions à nombre variable d'arguments, par exemple. À cause de cela, il est recommandé de forcer le type de la constante en le postfixant des attributs suivants :

  • U : la constante est non-signée (voir la section promotion pour comprendre les implications) ;
  • L : la constante est de type long au lieu de int ;
  • LL est une nouveauté C99 pour les constantes de type long long.

U peut être combiné à L et LL pour obtenir les types unsigned long et unsigned long long, respectivement. Lorsqu'une constante est suffixée, mais que sa valeur est trop grande pour le type demandé, le même processus de recherche de type « assez grand » est utilisé[7].

Débordement

Sur une machine donnée, un type entier a un domaine de valeurs fixe. Considérons qu'on travaille sur un PC en 32 bits, en complément à deux : sur un tel ordinateur, le type int varie souvent de -231 à 231-1. Cela permet de manipuler sans problème des valeurs dans ce domaine. Par contre, si on utilise des valeurs hors du domaine, par exemple 232, et qu'on essaye de la stocker dans une variable de type int sur une telle machine, que se passe-t-il ?

La réponse dépend du type:

  • Si on essaye d'enregistrer une valeur hors domaine dans une variable de type signé (comme dans l'exemple), la conversion n'est pas définie par le langage. Cela signifie que tout peut arriver.
  • Si on essaye d'enregistrer une valeur hors domaine dans une variable de type non signé (unsigned long, par exemple), la conversion se fait modulo la valeur maximale représentable par ce type + 1.

Exemples : On suppose que le type unsigned char est codé sur 8 bits. Alors une valeur de ce type peut aller de 0 à 28-1, soit 255. Par la règle précédente, les conversions se feront donc modulo 255 + 1, soit 256. On considère le programme suivant :

#include <stdio.h>

int main(void)
{
    unsigned char c = 300;
    /* %hhu sert à dire à printf() qu'on veut afficher un unsigned char */
    printf("La variable c vaut %hhu.\n", c);
    c = -5;
    printf("La variable c vaut %hhu.\n", c);
    return 0;
}

Le résultat du programme est :

La variable c vaut 44.
La variable c vaut 251.

En effet, on a 300 - 256 = 44 et -5 + 256 = 251.

De même :

#include <stdio.h>

int main(void)
{
    signed char c = 300;
    /* %hhd sert à dire à printf() qu'on veut afficher un signed char */
    printf("La variable c vaut %hhd.\n", c);
    c = -300;
    printf("La variable c vaut %hhd.\n", c);
    return 0;
}

Le résultat du programme peut alors être :

La variable c vaut 44.
La variable c vaut -44.

Sur une telle machine, les types signés sont traités de la même manière que les types non signés.

Si vous essayez de compiler ces deux programmes, votre compilateur pourra détecter les débordements et vous en avertir (GCC le fait).

Réels

Les nombres réels ne pouvant tous être représentés, sont approximés par des nombres à virgule flottante. Comme dans le cas des entiers, il existe plusieurs types de nombre à virgule flottante. En voici la liste triée par précision croissante :

  • float ;
  • double ;
  • long double.

La norme C90 était assez floue concernant les nombres à virgule flottante, leurs représentations, la précision des opérations, etc., ce qui fait que c'était un des domaines où la conception de programmes utilisant les nombres flottants était chose peu aisée. Le C99 a clarifié les choses en précisant qu'une implémentation C devait respecter la norme IEC 60559:1989 Arithmétique binaire en virgule flottante pour systèmes à microprocesseur. Cette norme (dérivée de IEEE 754) définit des formats de données pour les nombres à virgule flottante, ainsi que des opérations et fonctions sur ces nombres. Elle garantit, entre autres :

  • que certains types de données auront toujours le même format ;
  • et que les calculs effectués sur un type de donnée donneront toujours le même résultat.

Elle définit de plus comment sont gérés les cas exceptionnels, comme les infinis, les NaN (pour Not a Number, résultant par exemple de la division 0/0), etc.

Les types flottants du C correspondent aux type IEC 60559 de la manière suivante :

  • float : simple précision
  • double : double précision
  • long double : suivant l'implémentation, soit la double précision étendue, soit un type non-IEC 60559 (mais de précision au moins égale à double), soit double précision.

Constantes réelles

Une suite de caractères représente une constante à virgule flottante si :

  • c'est une suite de chiffres séparée par un caractère « point », cette séparation pouvant s'effectuer à n'importe quel endroit de la suite (0.0, .0 et 0. représentent tous les trois la valeur 0 de type double) ;
  • un nombre suivi d'un caractère « e » suivi d'un entier.

Dans le deuxième cas, le nombre peut être soit un entier, soit un réel du premier cas.

Les constantes sont de type double par défaut. Pour demander le type float, il faut la suffixer par f ou F, et pour le type long double par l ou L.

Arithmétique

Une attention particulière doit être portée sur la précision des types réels. Ces différents types ne font qu'approximer l'ensemble des nombres réels, avec une précision finie. Des erreurs d'arrondis sont à prévoir, ce qui est très problématique pour des domaines qui n'en tolèrent pas (notamment pour les applications financières, il est conseillé de ne pas utiliser de calcul en virgule flottante).

Le type float en particulier a une précision minimale, qui est bien souvent insuffisante. Voici un exemple classique d'erreur à ne pas faire qui illustre les problèmes liés à la précision des types flottants :

#include <stdio.h>

int main(void)
{
    float i = 0;
    int j;
    for (j = 0; j < 1000; j ++)
    {
        i += 0.1;
    }
    printf("i = %f\n", i);
    return 0;
}

Le résultat est 99,999046, ce qui montre que la précision du type float est en général mauvaise, d'autant plus que le nombre 0,1 n'est pas représentable de manière exacte en binaire[8]. Il est ainsi conseillé d'utiliser le type double à la place de float autant que possible. Dans ce cas de figure, il est préférable d'éviter les accumulations d'erreurs infinitésimales, en réécrivant le code de la manière suivante :

#include <stdio.h>

int main(void)
{
    float i;
    int j;
    for (j = 0; j < 1000; j++)
    {
        i = j * 0.1;
    }
    printf("i = %f\n", i);
    return 0;
}

C'est probablement plus coûteux, car on effectue un millier de multiplications au lieu d'un millier d'additions, mais cela permet d'avoir une précision nettement meilleure que le code précédent.

Pour plus d'informations sur ce domaine, un wikilivre Arithmétique flottante est disponible.

Caractères

À l'origine, le type permettant de représenter un caractère est char. Même si un char n'est plus toujours suffisant aujourd'hui pour représenter un caractère quelconque.

Ce type est un peu plus particulier que les autres, d'une part parce que sa taille définit l'unité de calcul pour les quantités de mémoire (et donc pour les tailles des autres types du langage) et d'autre part son domaine de valeur peut grandement varier de manière relativement inattendue.

Par définition, la taille du type char, notée sizeof(char), vaut toujours 1. Cependant, il faut faire attention : contrairement à ce qu'on pense souvent, un char au sens du C ne vaut pas toujours un octet. Il occupera au minimum 8 bits, mais il existe des architectures, relativement spécialisées il est vrai, ayant des char de 9 bits, de 16 bits, voire plus. Même si, dans une large majorité des cas, les compilateurs utilisent des char de 8 bits, à la fois par simplicité (les machines modernes fonctionnent généralement en 8, 16, 32 ou 64 bits) et pour éviter des problèmes de portabilité de code (beaucoup de codes C existants reposent sur l'hypothèse que les char font 8 bits, et risqueraient de ne pas marcher sur une autre architecture)[9]. Par simplification, nous utiliserons donc le terme octet la plupart du temps dans la suite de ce wikilivre.

Un autre piège de ce type est qu'il peut être de base (c'est-à-dire implicitement) signed ou unsigned, au choix du compilateur, ce qui peut s'avérer dangereux (c'est-à-dire difficile à maitriser). Considérez le code suivant :

/* Ce code peut ne pas fonctionner avec certains compilateurs */
char i;
for (i = 100; i >= 0; i --)
{
    /* ... */
}

Dans cet exemple, l'instruction for permet de faire itérer les valeurs entières de i de 100 à 0, incluses. On pourrait naïvement penser optimiser en utilisant un type char. Sauf que si ce type est implicitement unsigned, la condition i >= 0 sera toujours vraie, et tout ce que vous obtiendrez est une boucle infinie. Normalement, tout bon compilateur devrait vous avertir que la condition est toujours vraie et donc vous permettre de corriger en conséquence, plutôt que perdre des heures en débugage.

Constantes représentant un caractère

Une constante représentant un caractère (de type char) est délimitée par des apostrophes, comme par exemple 'a'. En fait pour le C, les caractères ne sont ni plus ni moins que des nombres entiers (ils sont de type int, mais leur valeur tiendra dans un type char), les deux étant parfaitement interchangeables. Cependant, comme dit plus haut, il ne s'agit généralement pas là des caractères Unicode que nous manipulons tous les jours, mais de caractère-octet dont la portée est réduite.

Un petit exemple :

#include <stdio.h>

int main(void)
{
    printf("Sur votre machine, la lettre 'a' a pour code %d.\n", 'a');
    return 0;
}

Le programme précédent donnera le résultat suivant dans un environnement ASCII :

Sur votre machine, la lettre 'a' a pour code 97.

Il est tout à fait autorisé d'écrire 'a' * 2 ou 'a' - 32.

Cette soustraction permet de convertir une minuscule ASCII en majuscule si le codage ASCII est utilisé, mais ne le permet pas si un autre codage est utilisé (comme ISO-8859-1 ou UTF-8).

Valeur des caractères-octets

La valeur représentée par cette constante est néanmoins dépendante des conventions de codages de caractères employées. À l'origine ces conventions étaient connues du système ; aujourd'hui, même si dans une écrasante majorité des cas, on se retrouvera avec un jeu de caractères augmentant l'ASCII et donc l'ISO-646. La code ASCII définit 96 glyphes de caractères portant les numéros 32 à 126, bien loin des milliers de caractères nécessaires pour les logiciels fonctionnant sur la planète entière et dans des langues nombreuses[10]. Cet ensemble est à peine suffisant pour couvrir l'anglais alors que plusieurs langues latines étaient visées à l'époque où l'ASCII et l'ISO-646 ont été définis, si bien que de nombreuses extensions sont par la suite apparues.

Par exemple le caractère « œ » (ligature du o et du e) a pour valeur :

  • 189 dans le jeu de caractères ISO-8859-15 (principalement utilisé pour les langues latines d'Europe, sous Unix) ;
  • 156 sur certaines variantes du jeu de caractères Windows 1252 (principalement utilisé pour les langues latines d'Europe, sous Windows) ;
  • 0xc5, 0x93 (deux octets, donc deux char) en l'UTF-8 ;
  • 207 avec l'encodage Mac Roman (Mac OS 9 et antérieur) ;
  • Et n'a pas d'équivalent ni en ISO-8859-1 ni en ASCII (le caractère 189 est le symbole « ½ », le 207 est le « Ï » et le 156 n'est pas utilisé).

Retenons simplement qu'Unicode se développe sur Internet, mais que des logiciels plus anciens ou embarqués peuvent fonctionner avec des jeux de caractères huit bits, plus limités. Il s'agit en fait d'une problématique concernant des sujets plus vastes, comme l' internationalisation, la portabilité et l' interopérabilité.

Le langage C reste relativement agnostique à ce niveau : les caractères sont des octets et les chaînes, une simple suite d'octets terminée par 0. Il laisse au système et au développeur le soin d'interpréter et de traiter les octets comme il se doit. Ceci peut être fait à l'aide de bibliothèques appropriées. Si on trouve ici où là de telles listes de bibliothèques [11], il est préférable de connaître le langage C avant de les utiliser.

Cet agnosticisme du langage C lui a permis de s'adapter aux nombreuses évolutions des conventions de codages des caractères.

Caractères non graphiques

Certains caractères ne sont pas graphiques. Par définition, on ne peut pas donc les écrire dans un code source de manière visible. Le langage C adopte la convention suivant pour désigner certains d'entre eux de manière littérale :

Constante | Caractère
'\'' | une apostrophe
'\"' | un guillemet
'\?' | un point d'interrogation
'\\' | un backslash
'\a' | un signal sonore (ou visuel)
'\b' | un espace arrière
'\f' | saut au début de la page suivante
'\n' | saut de ligne
'\r' | un retour chariot
'\t' | une tabulation
'\v' | une tabulation verticale

De plus, on peut écrire n'importe quelle valeur de caractère avec les expressions suivantes :

  • '\xHH', où chaque H représente un chiffre hexadécimal correspondant au code du caractère. Par exemple, '\x61' représente le caractère 'a' (minuscule) en ASCII (car 97 = 6 * 16 + 1) ;
  • "\xc5\x93" représente le caractère œ en UTF-8.
  • '\oOO', où chaque O représente un chiffre octal correspondant au code du caractère.

Trigraphe

Le langage C date d'une époque où le standard de codage des caractères était l'ISO-646, un standard encore plus ancien et plus incomplet que l'ASCII. Les caractères absent de l'ISO-646 pouvaient être simulés par une séquence de trois caractères.

L'ASCII s'étant répandu, cette fonctionnalité tombée en désuétude est en général à éviter, car très souvent inutile, et peut causer des bugs incompréhensibles au programmeur non averti. Néanmoins, de très rares programmes peuvent utiliser ce genre de fonctionnalités. Un trigraphe est simplement une suite de trois caractères dans le code source qui sera remplacée par un seul.

Cette fonctionnalité a été ajoutée au C pour supporter les architectures (systèmes) dont l'alphabet ne dispose pas de certains caractères qui sont nécessaires dans la syntaxe du C, comme les dièses ou les accolades.

Les substitutions suivantes sont faites partout dans le code source (y compris les chaînes de caractères) :

Trigraphe | Caractère
??= | #
??( | [
??) | ]
??< | {
??> | }
??/ | \
??' | ^
??! | |
??- | ~

Voici une manière de rendre illisible un programme utilisant les trigraphes :

??=include <stdio.h>

int main(void)
??<
   puts("Bonjour !");
   return 0;
??>

Par défaut, la plupart des compilateurs désactivent les trigraphes, au cas où vous ne seriez pas encore dissuadé de les utiliser.

Chaîne de caractères

Une chaîne de caractère, comme son nom l'indique, est une suite de caractères avec la particularité d'avoir un caractère nul (0) à la fin. Une chaîne de caractère est en fait implémentée en C avec un tableau de type char.

Ce chapitre vous donne les rudiments sur les chaînes de caractère pour une première approche. Pour en savoir plus sur

  • les rares fonctions de chaînes de caractères, voir Programmation C/Chaînes de caractères
  • les tableaux & pointeurs : Programmation_C/Tableaux#Cha.C3.AEnes_de_caract.C3.A8res
Programmation_C/Tableaux#Chaînes_de_caractères

Structure

En mémoire, une chaîne de caractère est représenté dans le langage C comme un tableau d'octet dont les valeurs dépendent de l'encodage utilisé ici, Unicode.

Amélie en Unicode formes NFC et NFD
Caractère représenté | A | m | é |  | l | i | e | '\0'
Unicode NFC | 0041 | 006d | 00e9 |  | 006c | 0069 | 0065 | 0
Unicode UTF-8 NFC | 41 | 6d | c3 a9 |  | 6c | 69 | 65 | 0
Unicode NFD | 0041 | 006d | 0065 | 0301 | 006c | 0069 | 0065 | 0
Unicode UTF-8 NFD | 41 | 6d | 65 | cc 81 | 6c | 69 | 65 | 0
Unicode NFD | A | m | e | ◌́ | l | i | e | '\0'

Chaîne littérale

On appelle une chaîne littérale la manière dont est définie une constante chaîne dans un code sources. Sous sa forme la plus simple, on déclare une chaîne comme une suite de caractères entre guillemets (double quote) :

"Ceci est une chaîne de caractère";
""; /* Chaîne vide */

Si la chaîne est trop longue, on peut aussi la couper sur plusieurs lignes :

"Ceci est une chaîne de caractère, " /* pas de ; */
"déclarée sur plusieurs lignes.";

Deux chaînes côtes à côtes (modulo les espaces et les commentaires) seront concaténées par le compilateur. De plus on peut utiliser le caractère barre oblique inverse (\) pour annuler la signification spéciale de certains caractères ou utiliser des caractères spéciaux (C.f liste ci-dessus).

"Une chaîne avec des \"guillemets\" et une barre oblique (\\)\n";

Il est aussi possible d'utiliser la notation hexadécimale et octale pour décrire des caractéres dans la chaîne, il faut néanmoins faire attention avec la notation hexadécimale, car la définition peut s'étendre sur plus de 2 caractéres. En fait, tous les caractères suivant le \x et appartenant à l'ensemble "0123456789abcdefABCDEF" seront utilisés pour déterminer la valeur du caractère (du fait que les caractères d'une chaîne peuvent faire plus qu'un octet). Cela peut produire certains effets de bords comme :

/* Ce code contient un effet de bord inattendu */
"\x00abcdefghijklmnopqrstuvzxyz"

Ce code n'insèrera pas un caractère 0 au début de la chaine, mais toute la séquence "\x00abcdef" servira à calculer la valeur du caractère (même si le résultat sera tronqué pour tenir dans un type char). On peut éviter cela en utilisant la concaténation de chaînes constantes :

"\x00" "abcdefghijklmnopqrstuvzxyz"

Enfin, les chaînes de caractères faisant appel aux concepts de pointeur, tableau et de zone mémoire statique, leur utilisation plus poussée sera décrite dans la section dédiée aux tableaux.

À noter la représentation obsolète des chaînes de caractères multilignes. À éviter dans la mesure du possible :

/* Ce code est obsolete et a eviter */
"Ceci est une chaîne de caractère,\
déclarée sur plusieurs lignes";

Booléens

Le langage (jusqu'à la norme C99) ne fournit pas de type booléen. La valeur entière 0 prend la valeur de vérité faux et toutes les autres valeurs entières prennent la valeur de vérité vrai.

La norme C99 a introduit le type _Bool, qui peut contenir les valeurs 0 et 1. Elle a aussi ajouté l'en-tête <stdbool.h>, qui définit le type bool qui est un raccourci pour _Bool, et les valeurs true et false[12]. Néanmoins, ces nouveautés du C99 ne sont pas très utilisées, les habitudes ayant été prises d'utiliser 0 et différent de zéro pour les booléens en C.

Nota : toute expression utilisant des opérateurs booléens (voir opérateurs), retourne 1 si l'expression est vraie et 0 si elle est fausse, ce qui rend quasiment inutile l'usage du type booléen.

Vide

En plus de ces types, le langage C fournit un autre type, void qui représente rien, le vide. Il n'est pas possible de déclarer une variable de type void. Nous verrons l'utilité de ce type lorsque nous parlerons de fonctions et de pointeurs.

Classes de stockage

Il est possible de construire des types dérivés des types de base du langage en utilisant plusieurs combinaisons, deux étant illustrées dans ce chapitre: les classes de stockage et les qualificateurs.

Classe de stockage

Le langage C permet de spécifier, avant le type d'une variable, un certain nombre de classes de stockage :

  • auto : pour les variables locales ;
  • extern : déclare une variable sans la définir ;
  • register : demande au compilateur de faire tout son possible pour utiliser un registre processeur pour cette variable ;
  • static : rend une définition de variable persistante.

Les classes static et extern sont, de loin, les plus utilisées. register est d'une utilité limitée, et auto est maintenant obsolète.

Une variable, ou un paramètre de fonction, ne peut avoir qu'au plus une classe de stockage.

Classe 'static'

L'effet de la classe 'static' dépend de l'endroit où l'objet est déclaré :

  • Objet local à une fonction : la valeur de la variable sera persistante entre les différents appels de la fonction. La variable ne sera visible que dans la fonction, mais ne sera pas réinitialisée à chaque appel de la fonction. L'intérêt est de garantir une certaine encapsulation, afin d'éviter des usages multiples d'une variable globale. Qui plus est, cela permet d'avoir plusieurs fois le même nom, dans des fonctions différentes.

Exemple :

#include <stdio.h>

void f(void)
{
    static int i = 0; /* i sera initialisée à 0 à la compilation seulement */
    int j = 0; /* j sera initialisée à chaque appel de f */;
    i++;
    j++;
    printf("i vaut %d et j vaut %d.\n", i, j);
}

int main(void)
{
    f();
    f();
    f();
    return 0;
}

Résultat d'exécution du code ci dessus :

i vaut 1 et j vaut 1.
i vaut 2 et j vaut 1.
i vaut 3 et j vaut 1.
  • Objet global et fonction : comme une variable globale est déjà persistante, le mot-clé static aura pour effet de limiter la portée de la variable ou de la fonction au seul fichier où elle est déclarée, toujours dans le but de garantir un certain niveau d'encapsulation.

Une variable de classe statique est initialisée au moment de la compilation à zéro par défaut (contrairement aux variables dynamiques qui ont une valeur initiale indéterminée). Elle peut être initialisée explicitement à n'importe quelle valeur constante.

À faire...

utilisation de static en C99 pour les tableaux en paramètres de fonctions

Classe 'extern'

extern permet de déclarer une variable sans la définir. C'est utile pour la compilation séparée, pour définir une variable ou une fonction dans un fichier, en permettant à des fonctions contenues dans d'autres fichiers d'y accéder.

Toutes les variables globales et fonctions qui ne sont pas déclarées (ou définies) static sont externes par défaut.

static et extern sont employés pour distinguer, dans un fichier C, les objets et fonctions « privés », qui ne doivent être utilisés que depuis l'intérieur du fichier, et ceux qui sont « publics », et pourront être accessibles depuis d'autres fichiers.

Classe 'register'

L'usage de ce mot clé est utile dans un contexte de logiciel embarqué. Indique que la variable devrait être stockée dans un registre du processeur. Cela permet de gagner en performance par rapport à des variables qui seraient stockées dans un espace mémoire beaucoup moins rapide, comme une pile placée en mémoire vive.

Ce mot-clé a deux limitations principales :

  • Les registres du processeur sont limités. Leur nombre peut varier en fonction du processeur. Sur un PC (d'architecture AMD64?), il sont au nombre de 13, dont seulement 4 servent au stockage (EAX,EBX,ECX,EDX). Il est donc inutile de déclarer une structure entière ou un tableau avec le mot clé register.
  • Qui plus est, les variables placées dans des registres sont forcément locales à des fonctions ; on ne peut pas définir une variable globale en tant que registre.

Aujourd'hui, ce mot-clé est déconseillé sauf pour des cas particuliers, les compilateurs modernes sachant généralement mieux que le programmeur comment optimiser et quelles variables placer dans les registres.

#include <stdio.h>

int main(void)
{
    register short i, j;
    for (i = 1; i < 1000; ++i)
    {
        for(j = 1; j < 1000; ++j)
        {
            printf("\n %d %d", i, j);
        }
    }
    return 0;
}

Classe 'auto'

Cette classe est un héritage du langage B. En C, ce mot-clé sert pour les variables locales à une fonction non-statiques, dites aussi automatiques. Mais une variable déclarée localement à une fonction sans qualificateur static étant implicitement automatique, ce mot-clé est inutile en C.

Qualificateurs

Le C définit trois qualificateurs pouvant influer sur une variable :

  • const : pour définir une variable dont la valeur ne devrait jamais changer ;
  • restrict : permet une optimisation pour la gestion des pointeurs ;
  • volatile : désigne une variable pouvant être modifiée notamment par une source externe indépendante du programme.

Une variable, ou un paramètre de fonction, peut avoir aucun, un, deux, ou les trois qualificateurs (certaines combinaisons n'auraient que peu de sens, mais sont autorisées).

Qualificateur 'const'

La classe const ne déclare pas une vraie constante, mais indique au compilateur que la valeur de la variable ne doit pas changer. Il est donc impératif d'assigner une valeur à la déclaration de la variable, sans quoi toute tentative de modification ultérieure entrainera une erreur de la part du compilateur :

Avertissement Ce code contient une erreur volontaire !
const int i = 0;
i = 1; /* erreur*/

En fait, le mot-clé const est beaucoup plus utilisé avec des pointeurs. Pour indiquer qu'on ne modifie pas l'objet pointé, il est bon de spécificier le mot-clé const :

void fonction( const char * pointeur )
{
        pointeur[0] = 0;     /* erreur*/
        pointeur    = "Nouvelle chaine de caractères";
}

Dans cet exemple, on indique que l'objet pointé ne sera pas modifié. Pour indiquer que la valeur elle-même du pointeur est constante, il faut déclarer la variable de la sorte :

Avertissement Ce code contient une erreur volontaire !
char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/

Encore plus subtil, on peut mélanger les deux :

Avertissement Ce code contient plusieurs erreurs volontaires !
const char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/
pointeur[0] = 0; /* erreur*/

Cette dernière forme est néanmoins rarement usitée. En outre ce dernier exemple présente un autre problème qui est la modification d'une chaine de caractères « en dur », qui sont la plupart du temps placées dans la section lecture seule du programme et donc inaltérables.

Qualificateur 'volatile'

Ce mot-clé sert à spécifier au compilateur que la variable peut être modifiée à son insu. Cela annule toute optimisation que le compilateur pourrait faire, et l'oblige à procéder à chaque lecture ou écriture dans une telle variable tel que le programmeur l'a écrit dans le code. Ceci a de multiples utilisations :

  • pour les coordonnées d'un pointeur de souris qui seraient modifiées par un autre programme ;
  • pour la gestion des signaux (voir Gestion des signaux) ;
  • pour de la programmation avec de multiples fils d'exécution qui doivent communiquer entre eux ;
  • pour désigner des registres matériels qui peuvent être accédés depuis un programme C (une horloge, par exemple), mais dont la valeur peut changer indépendamment du programme ;
  • etc.

On peut combiner const et volatile dans certaines situations. Par exemple :

extern const volatile int horloge_temps_reel;

déclare une variable entière, qu'on ne peut modifier à partir du programme, mais dont la valeur peut changer quand même. Elle pourrait désigner une valeur incrémentée régulièrement par une horloge interne.

Qualificateur 'restrict'

Introduit par C99, ce mot-clé s'applique aux déclarations de pointeurs uniquement. Avec restrict, le programmeur certifie au compilateur que le pointeur déclaré sera le seul à pointer sur une zone mémoire. Cela permettra au compilateur d'effectuer des optimisations qu'il n'aurait pas pu deviner autrement. Le programmeur ne doit pas mentir sous peine de problèmes...

Par exemple :

int* restrict pZone;

Opérateurs

Les opérateurs du C

Les opérateurs du C permettent de former des expressions, expressions qui diront quoi faire à votre programme. On peut voir un programme C comme étant composé de trois catégories d'instructions :

  • Les déclarations et définitions (variables, fonctions, types) : déclarent et définissent les objets que pourront manipuler le programme.
  • Les expressions : manipulent les déclarations, via les opérateurs.
  • Les instructions : manipulent les expressions pour leur donner une signification particulière (test, boucle, saut, ...).

Les déclarations de variables ont en partie été décrites dans les chapitres précédents. Les expressions seront en partie décrites dans celui-ci (les opérateurs liés aux pointeurs, tableaux, structures et fonctions seront décrit dans des chapitres dédiés), et les instructions seront décrites au cours des chapitres suivants.

Les expressions en C sont en fait très génériques, elles peuvent inclure des opérations arithmétiques classiques (addition, soustraction, division, modulo, ...), des expressions booléennes (OU logique, ET logique, OU exclusif, ...), des comparaisons (égalité, inégalité, différence, ...) et même des affectations (copie, auto-incrément, ...). Toutes ces opérations peuvent s'effectuer en une seule expression, il suffit d'appliquer les bons opérateurs sur les bons opérandes.

Les opérateurs binaires et ternaires utilisent une notation infixe (l'opérateur se trouve entre les 2 ou 3 opérandes). Les opérateurs unaires s'écrivent de manière préfixé (avant l'opérande), à l'exception des opérateurs ++ et -- qui peuvent s'écrire de manière préfixée ou suffixée (avec une différence subtile, décrite ci-après).

La priorité (quel opérateur est appliqué avant, en l'absence de parenthèses explicite) et l'associativité (dans quel ordre sont traités les arguments des opérateurs ayant la même priorité) sont résumées dans la table suivante (par ordre décroissant de priorité - les opérateurs décrits dans un autre chapitre ont un lien dédié) :

opérateur | parité | associativité | description
( ) |  | gauche vers la droite (GD) | parenthésage
() [] . -> |  | GD | appel de fonction, index de tableau, membre de structure, pointe sur membre de structure
! | unaire | droite vers la gauche (DG) | négation booléenne
~ | unaire | DG | négation binaire
++ -- | unaire | DG | incrémentation et décrémentation
- | unaire | DG | opposé
(type) | unaire | DG | opérateur de transtypage (cast)
* | unaire | DG | opérateur de déréférençage
& | unaire | DG | opérateur de référençage
sizeof | unaire | DG | fournit la taille en nombre de "char" de l'expression (souvent en octet mais pas toujours, mais sizeof(char) == 1 par définition, voir Caractères)
* / % | binaire | GD | multiplication, division, modulo (reste de la division)
+ - | binaire | GD | addition, soustraction
>> << | binaire | GD | décalages de bits
> >= < <= | binaire | GD | comparaisons
== != | binaire | GD | égalité/différence
& | binaire | GD | et binaire
^ | binaire | GD | ou exclusif binaire
| | binaire | GD | ou inclusif binaire
&& | binaire | GD | et logique avec séquencement
|| | binaire | GD | ou logique avec séquencement
? : | ternaire | DG | si...alors...sinon
= += -= *= /= %= ^= &= |= >>= <<= | binaire | DG | affectation
, | binaire | GD | séquencement

Post/pré incrémentation/décrémentation

C'est un concept quelque peu sibyllin du C. Incrémenter ou décrémenter de un est une opération extrêmement courante. Écrire à chaque fois 'variable = variable + 1' peut-être très pénible à la longue. Le langage C a donc introduit des opérateurs raccourcis pour décrémenter ou incrémenter n'importe quel type atomique (gérable directement par le processeur : c'est à dire pas par un tableau, ni une structure ou une union). Il s'agit des opérateurs '++' et '--', qui peuvent être utilisés de manière préfixée ou suffixée (avant ou après la variable).

Utilisé de manière préfixée, l'opérateur incrémente/décrémente la variable, puis retourne la valeur de celle-ci. En fait, les expressions (++E) et (--E) sont équivalentes respectivement aux expressions (E+=1) et (E-=1).

Par contre, utilisé de manière suffixée, l'opérateur retourne la valeur originale avant de modifier la valeur de la variable.

int i = 0, j;

j = i++; /* j vaut  0 et i vaut  1 */
j = --i; /* j vaut 0 et i vaut 0 */

Il est important de noter que le langage ne fait que garantir qu'une variable post-incrémentée ou post-décrémentée acquiert sa nouvelle valeur entre le résultat de l'opérateur et le prochain point de séquencement atteint (généralement la fin d'une instruction). Mais on ne sait pas vraiment quand entre les deux. Ainsi, si l'objet sur lequel s'applique un tel opérateur apparaît plusieurs fois avant un point de séquencement, alors le résultat est imprévisible et son comportement peut changer simplement en changeant les options d'un même compilateur :

Avertissement Ce code contient plusieurs erreurs volontaires !
/* Le code qui suit est imprévisible */
int i = 0;
i = i++;
Avertissement Ce code contient une erreur volontaire !
/* Et celui-là d'après vous ? */
int i = 0;
i = ++i;

Contrairement à ce à quoi vous pouviez vous attendre, le bout de code ci-dessous n'est pas mieux défini que celui d'au-dessus. La valeur de i y est modifiée une première fois lors de son incrémentation, puis une deuxième fois par l'opérateur d'affectation =. Or, un autre moyen de générer un comportement indéfini est de modifier la valeur d'une variable plusieurs fois entre deux points de séquencement.

Promotion entière

La promotion entière, à ne pas confondre avec la conversion automatique de type, fait que tous les types plus petits ou égaux à int (char, short, champs de bits, type énuméré) sont convertis (promus) en int ou unsigned avant toute opération. Ainsi, dans l'exemple suivant, a + b est calculé avec des int et le résultat est de type int :

short sum(short a, short b) {
    return a + b; /* équivaut à : return (int)a + (int)b; */
}

Le compilateur peut émettre un avertissement du fait que le résultat int est converti en short pour être retourné, et cette conversion peut causer une perte de précision (par exemple si int a une largeur de 32 bits et short de 16 bits).

La promotion se fait vers unsigned lorsqu'un int ne peut pas représenter le type promu.

Conversion automatique

La conversion est un mécanisme qui permet de convertir implicitement les nombres dans le format le plus grand utilisé dans l'expression.

L'exemple classique est le mélange des nombres réels avec des nombres entiers. Par exemple l'expression '2 / 3.' est de type double et vaudra effectivement deux tiers (0,666666...). L'expression '2 * a / 3' calculera les deux tiers de la variable 'a', et arrondira automatiquement à l'entier par défaut, les calculs ne faisant intervenir que des instructions sur les entiers (en supposant que 'a' soit un entier). À noter que l'expression '2 / 3 * a' vaudra... toujours zéro !

Plus subtile, l'expression '2 * a / 3.', en supposant toujours que a soit un entier, effectuera une multiplication entière entre 2 et a, puis promouvra le résultat en réel (double) puis effectuera la division réelle avec trois, pour obtenir un résultat réel lui-aussi.

Enfin un cas où les promotions peuvent surprendre, c'est lorsqu'on mélange des entiers signés et non-signés, plus particulièrement dans les comparaisons. Considérez le code suivant :

/* Ce code contient un effet de bord sibyllin */
unsigned long a = 23;
signed char b = -23;

printf( "a %c b\n", a < b ? '<' : (a == b ? '=' : '>') );

De toute évidence a est supérieur à b dans cet exemple. Et pourtant, si ce code est exécuté sur certaines architectures, il affichera a < b, justement à cause de cette conversion.

Tout d'abord dans la comparaison a < b, le type de a est « le plus grand », donc b est promu en unsigned long. C'est en fait ça le problème : -23 est une valeur négative, et la conversion d'une valeur négative en un type non signé se fait modulo la valeur maximale représentable par le type non signé + 1. Si la valeur maximale pour le type unsigned long est 232-1, alors -23 est converti modulo 232, et devient 4294967273 (soit 232-23). Dans ce cas là effectivement 23 < 4294967273, d'où ce résultat surprenant.

À noter qu'on aurait eu le même problème si le type de b était signed long. En fait les types signés sont promus en non-signés, s'il y a au moins une variable non-signée dans l'expression, et que le type non-signé est "plus grand" que le type signé.

Dans ce cas présent, il faut effectuer soi-même le transtypage :

printf( "a %c b\n", (long) a < b ? '<' : ((long)a == b ? '=' : '>') );

La norme définit précisément les conversions arithmétiques et les promotions entières, se référer à elle pour les détails.

Évaluation des expressions booléennes

Le C ne possède pas de type booléen dédié[13]. Dans ce langage, n'importe quelle valeur différente de zéro est considérée vraie, zéro étant considéré comme faux. Ce qui veut dire que n'importe quelle expression peut être utilisée à l'intérieur des tests (entier, réels, pointeurs, tableaux, etc.). Cela peut conduire à des expressions pas toujours très claires, comme :

int a;
a = une_fonction();
if (a)
{
    /* ... */
}

Ce type d'écriture simplifiée a ses adeptes, mais elle peut aussi s'écrire de la manière suivante:

int a;
a = une_fonction();
if (a != 0)
{
    /* ... */
}

Cette seconde écriture, sémantiquement équivalente à la première, rend explicite le test qui est effectué (on compare la valeur de la variable a à zéro).

Par ailleurs, cette absence de type booléen rend aussi valide le code suivant:

int a = 0;
int b = 2;

if (a = b)
{
        /* Le code qui suit sera toujours exécuté ... */
}

Dans cet exemple, il y a un seul caractère = entre a et b, donc on ne teste pas si a est égal à b, mais on affecte la valeur de la variable b à la variable a. Le résultat de l'expression étant 2, elle est donc toujours considérée vraie. Ce genre de raccourci est en général à éviter. En effet, il est facile de faire une faute de frappe et d'oublier de taper un caractère =. Comme le code résultant est toujours valide au sens du C, cette erreur peut ne pas être vue immédiatement. Pour aider les développeurs à détecter ce genre d'erreurs, de nombreux compilateurs émettent un avertissement quand un tel code leur est présenté. Une manière de faire taire ces avertissements, une fois qu'on s'est assuré qu'il ne s'agit pas d'une erreur, est de mettre entre parenthèses l'affectation:

/* On veut faire une affectation ici. */
/* doublement des parenthèses pour supprimer l'avertissement du compilateur*/
if ((a = b))
{
    /* ... */
}

Comme un développeur devant maintenir un programme contenant un test if (a = b) { /* ... */ } se demandera si le développeur précédent n'a pas fait une faute de frappe, il est préférable d'éviter autant que possible ce genre de situations et, s'il est nécessaire (ce qui a très peu de chances d'arriver), de la commenter. En effet, même un code if ((a = b)) { /* ... */ } non commenté doit être lu avec attention, car le développeur précédent peut avoir ajouté les parenthèses juste pour faire taire le compilateur, sans se rendre compte d'une erreur. Dans tous les cas, la manière la plus sûre est de décomposer ainsi:

a = b;
if (a != 0)
{
    /* ... */
}

Une autre technique classique, lorsqu'une comparaison fait intervenir une constante, est de mettre la constante à gauche. De cette manière, si la comparaison se transforme par mégarde en affectation, cela provoquera une erreur à la compilation :

if (0 == b)
{
    /* Une instruction "0 = b" ne passerait pas */
}

Les opérateurs logiques de comparaisons (&& et ||, similaires sémantiquement à leur équivalent binaire & et |) évaluent leurs opérandes en circuit court. Dans le cas du ET logique (&&), si l'opérande gauche s'évalue à faux, on sait déjà que le résultat du ET sera faux et donc ce n'est pas la peine d'évaluer l'opérande droite. De la même manière si l'opérande gauche d'un OU logique (||) est évalué à vrai, le résultat sera aussi vrai et donc l'évaluation de l'opérande droite est inutile. Ceci permet d'écrire des expressions de ce genre, sans générer d'exception de l'unité arithmétique du processeur :

if (z != 0 && a / z < 10)
{
    printf("Tout va bien\n");
}

Et voila !

On réduit :

if (string)
 len = strlen(string);
else
 len = 0;

par

len = string ? strlen(string) : 0;

C'est à dire que len égale si string à la longueur de string si non 0 ;)

L'ordinateur calcule très vite et très bien, c'est pour ça que l'utiliser reste un atout.

Les calculs ne sont pour lui que des instructions simples et utilisant peu de ressource (mul, div, add, sub, inc, xor, ...).

Structures de contrôle - tests

Les tests permettent d'effectuer des opérations différentes suivant qu'une condition est vérifiée ou non.

Test if

Les sauts conditionnels permettent de réaliser une instruction si une condition est vérifiée. Si la condition n'est pas vérifiée, l'exécution se poursuit séquentiellement.

Syntaxe

Première forme :

/* Ce code est volontairement incomplet */
    if (condition)
        instruction
    instruction suivante

Si la condition est vérifiée, alors on exécute l'instruction, sinon on exécute l'instruction suivante.

/* Ce code est volontairement incomplet*/
    if (condition) instruction; /* ... */
    instruction suivante; /* ... */ ;
Deuxième forme

Deuxième forme : if ( condition )

{

instructions

}else

/* Ce code est volontairement incomplet */
    if (condition)
        instructions
    instruction suivante

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute exécute l'instruction suivante.

/* Ce code est volontairement incomplet */
    if (condition)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    }
    instruction suivante; /* ... */ ;
Troisième forme

Troisième forme :

/* Ce code est volontairement incomplet */
    if (condition)
        instructions
    else
        autre instructions
    instruction suivante; /* ... */ ;

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute l'autre bloc.

    /* Ce code est volontairement incomplet*/
    if (condition)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    } else {
       instruction 3; /* ... */
       instruction 4; /* ... */
    }
    instruction suivante; /* ... */ ;

Quatrième forme :

    /* Ce code est volontairement incomplet*/
    if (condition 1)
        instructions
    else if (condition 2)
        autre instructions
    else
        autre instructions
    instruction suivante; /* ... */ ;

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute l'autre if comme la troisième forme.

/* Ce code est volontairement incomplet */
    if (condition 1)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    } else if (condition 2) {
       instruction 3; /* ... */
       instruction 4; /* ... */
    } else {
       instruction 5; /* ... */
       instruction 6; /* ... */
    }
    instruction suivante; /* ... */ ;

Exemple de code :

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int a;
    a = 3; /* assignation d'une valeur pour l'exemple*/

    if (a >= 5)
        printf("Le nombre a est supérieur ou égal à 5 \n");
    else
        printf("Le nombre a est inférieur à 5 \n");
    return 0;
}

D'ailleurs, lorsqu'un if contient une instruction (for, do, while, switch, goto, return, etc.) il est conseillé de la mettre entre accolades.

Test switch

Cette instruction permet de tester si une expression coïncide avec un certain nombre de constantes, et d'exécuter une action par défaut dans le cas où aucune valeur ne correspond à celle de l'expression. Cela permet un traitement beaucoup plus efficace et lisible qu'une succession de if/else imbriqués. Insistons sur le terme constante : il est en effet impossible d'utiliser des expressions dont la valeur n'est pas connue à la compilation (c'est à dire de variable dans les instructions case).

Syntaxe

switch (expression)
{
    case valeur1:
        bloc1
    case valeur2:
        bloc2
    /*...*/
    case valeurN:
        blocN
    default:
        blocD
}

Compare la valeur de l'expression à celles de valeur1, valeur2, ..., valeurN. En cas d'égalité entre expression et valeurI les blocs sont exécutés séquentiellement à partir de blocI, et ce jusqu'à la fin de l'instruction switch. Si expression est égale à valeur2, dans cet exemple, les blocs bloc2 à blocN et même blocD seront exécutés. Pour empêcher ce comportement on utilise l'instruction break, que l'on peut placer à n'importe quel endroit pour sortir (aller à la fin) de l'instruction switch. En général, on retrouve plus fréquemment l'instruction switch écrite de la sorte :

switch (expression)
{
    case valeur1:
        bloc1
        break;
    case valeur2:
        bloc2
        break;
    /*...*/
    case valeurN:
        blocN
        /* pas de break; */
    default:
        blocD
}

C'est en fait tellement rare de ne pas mettre de break entre les différents cas, qu'il est conseillé de mettre un commentaire pour les cas où cette instruction est délibérément omise, ça permet de bien signaler au lecteur qu'il ne s'agit pas d'un oubli.

Expression conditionnelle

condition ? expression_si_vrai : expression_si_faux ;

Exemple

#include <stdio.h>

int main(int argc, char * argv[])
{
    /*
        program                      argc=1  argv[0]="program"
        program argument             argc=2  argv[0]="program"  argv[1]="argument"
        program argument argument    argc=3  argv[0]="program"  argv[1]="argument"  argv[2]="argument"
        ...
    */
    printf("%s\n", argc < 2 ? "Vous n'avez pas donné d'argument." : "Vous avez donné au moins un argument.");
    return 0;
}

Ce mini-programme teste si le nombre d'arguments passé à main est inférieur à 2 avec l'expression argc < 2 et renvoie "vous n'avez pas donné d'argument" si l'expression est vraie et "vous avez donné au moins un argument" sinon. Le résultat de l'évaluation est alors passé à la fonction printf qui affiche le résultat. (si argc vaut 2 alors il y a 1 argument : voir "la fonction main" dans le chapitre "fonctions")

On note le manque de clarté de l'exemple: l'opérateur ternaire ne doit être utilisé que dans de rares cas, c'est à dire lorsque son utilisation ne se fait pas au détriment de la lisibilité du code.

Structures de contrôle - itérations

Boucle for

Syntaxe

for (initialisation ; condition ; itération)
    bloc

Une boucle for commence par l' initialisation, puis exécute le bloc de code tant que la condition est vérifiée et en appelant l'itération après chaque bloc.

On peut tout de suite remarquer que la boucle for du C est beaucoup plus puissante qu'une boucle for classique, dans le sens où elle ne correspond pas forcément à un nombre fini de tours de boucle.

La norme C99 permet maintenant de déclarer des variables dans la partie initialisation de la boucle for. Ces variables ne peuvent qu'être de classe automatic ou register. Leur portée est limitée à la boucle. Elles n'interfèrent pas avec les variables de même nom déclarées en dehors de la boucle. Leur utilisation pourrait conduire à de possibles optimisations du code par le compilateur.

Exemples

Exemple 1
int i;
for (i = 0 ; i < 10 ; i++)
{
    printf("%d\n", i);
}

En C++ :

for (int i = 0 ; i < 10 ; i++)
{
    printf("%d\n", i);
}

Cet exemple est une boucle for normale, l'indice de boucle i est incrémenté à chaque tour et le bloc ne fait qu'afficher l'indice. Ce code affiche donc les 10 premiers entiers en partant de 0. Le bloc de code étant réduit à une instruction, on aurait pu écrire :

int i;
for (i = 0 ; i < 10 ; i++)
    printf("%d\n", i);

Exemple 2

int i = 0;
for( ; i != 1 ; i = rand() % 4)
{
        printf("je continue\n");
}

Ce deuxième exemple montre plusieurs choses :

  1. l'initialisation de la boucle a été omise, en fait n'importe laquelle des trois parties peut l'être (et même les trois à la fois, ce qui résulte en une boucle infinie) ;
  2. l'expression itération est réellement une expression, on peut y faire appel à des fonctions ;
  3. le nombre d'itérations n'est pas fixe, le programme affichera « je continue » un nombre (pseudo-)aléatoire de fois.
for(;;) crée une boucle infinie.

Boucle while

Syntaxe

while (condition)
    bloc

Une boucle while exécute le bloc d'instructions tant que la condition est vérifiée.

Exemple

int i = 0;
while (i < 10)
{
        printf("%d\n", i);
        i = i + 1;
}

Cet exemple est le même que celui vu précédement pour la boucle for, il affiche les dix premiers entiers à partir de 0.

Boucle do-while

Syntaxe

do
    bloc
while (condition);

Cette boucle est une légère variante de la précédente. Elle exécute donc le bloc au moins une fois, et ce jusqu'à ce que la condition soit fausse (ou tant que la condition est vraie).

Exemple 1

#include <stdio.h>

int main(void)
{
    int i = 0;
    do
    {
        printf("%d\n", i);
        i = i + 1;
    } while (i < 10);
    return 0;
}

Exemple identique à celui de la boucle while au dessus, on affiche les nombres de 0 jusqu'à 9.

Exemple 2

#include <stdio.h>

int main(void)
{
    char c;
    do
    {
        printf("Veuillez taper la lettre 'o'\n");
        scanf("%c", &c);
    } while (c != 'o');
    return 0;
}

Cette fois on voit l'utilité de la boucle do-while, la variable est initialisée dans la boucle. On doit tester sa valeur à la fin de l'itération. Tant que l'utilisateur n'aura pas tapé la lettre o, le programme redemandera de taper la lettre.

Exemple 3

http://fr.wikibooks.org/wiki/Discussion:Programmation_C/Pointeurs#Exemple_Complet_sur_les_boucles.2C_les_tableaux_et_les_pointeurs

Arrêt et continuation des boucles

Il arrive fréquemment qu'en évaluant un test à l'intérieur d'une boucle, on aimerait arrêter brutalement la boucle ou alors « sauter » certains cas non significatifs. C'est le rôle des instructions break et continue.

  • break permet de sortir immédiatement d'une boucle for, while ou do-while. À noter que si la boucle se trouve elle-même dans une autre boucle, seule la boucle où l'instruction break se trouvait est stoppée.
  • continue permet de recommencer la boucle depuis le début du bloc. Dans le cas de la boucle for, le bloc lié à l'incrémentation sera exécuté, puis dans tous les cas, la condition sera testée.

Saut inconditionnel (goto)

Cette instruction permet de continuer l'exécution du programme à un autre endroit, dans la même fonction. On l'utilise de la manière suivante :

goto label;

Où label est un identificateur quelconque. Cet identificateur devra être défini quelque part dans la même fonction, avec la syntaxe suivante :

label:

Ce label peut être mis à n'importe quel endroit dans la fonction, y compris avant ou après le goto, ou dans un autre bloc que là où sont utilisés la ou les instructions goto pointant vers ce label. Les labels doivent être uniques au sein d'une même fonction, mais on peut les réutiliser dans une autre fonction.

Un cas où l'emploi d'une instruction goto est tout à fait justifié, par exemple, est pour simuler le déclenchement d'une exception, pour sortir rapidement de plusieurs boucles imbriquées.

void boucle(void)
{
    while (1)
    {
        struct Evenement_t * event;

        attend_evenement();

        while ((event = retire_evenement()))
        {
            switch (event->type) {
            case FIN_TRAITEMENT:
                goto fin;
            /* ... */
            }
        }
    }
    fin:
    /* ... */
}

Dans ce cas de figure, il serait possible d'utiliser aussi un booléen qui indique que le traitement est terminé, pour forcer la sortie des boucles. L'utilisation de goto dans cet exemple ne nuisant pas à la lisibilité du programme et étant tout aussi efficace, il est tout à fait légitime de l'utiliser.

Il faut cependant noter que cette instruction est souvent considérée comme à éviter. En effet, elle provoque un saut dans le code, qui se « voit » beaucoup moins bien que les structures de contrôle comme if ou while, par exemple. De fait, la pratique a montré qu'un abus de son utilisation rend un code source rapidement peu compréhensible, et difficilement maintenable. L'article de Edsger W. Dijsktra Go to considered harmful anglais, datant de 1968, est l'exemple le plus connu de cette critique, qui a donné naissance à la programmation structurée.

Fonctions et procédures

Définition

Le code suivant définit une fonction fonction renvoyant une valeur de type type_retour et prenant N arguments, par1 de type type1, par2 de type type2, etc.

type_retour fonction(type1 par1, type2 par2, /* ..., */ typeN parN)
{
        /* Déclarations de variables ... */
        /* Instructions ... */
}

L'exécution d'une fonction se termine soit lorsque l'accolade fermante est atteinte, soit lorsque le mot clef return est rencontré. La valeur renvoyée par une fonction est donnée comme paramètre à return. Une procédure est une fonction renvoyant void, dans ce cas return est appelé sans paramètre.

Les passages des arguments aux fonctions se font toujours par valeur. Si on veut modifier la valeur d'un argument passé en paramètre à une fonction, en dehors de cette même fonction, il faut utiliser des pointeurs.

Déclaration par prototype

Le prototype d'une fonction correspond simplement à son en-tête (tout ce qui précède la première accolade ouvrante). C'est-à-dire son nom, son type de retour et les types des différents paramètres. Cela permet au compilateur de vérifier que la fonction est appelée avec le bon nombre de paramètres et surtout avec les bons types. La ligne suivante déclare la fonction fonction, mais sans la définir :

type_retour nom_fonction(type1, type2, /* ..., */ typeN);

À noter que les noms des paramètres peuvent être omis et que la déclaration doit se terminer par un point-virgule (;), sans quoi vous pourrez vous attendre à une cascade d'erreurs.

Absence des paramètres

Avant la normalisation par l'ANSI, il était possible de faire une déclaration partielle d'une fonction, en spécifiant son type de retour, mais pas ses paramètres:

int f();

Cette déclaration ne dit rien sur les éventuels paramètes de la fonction f, sur leur nombre ou leur type, au contraire de :

int g(void);

qui précise que la fonction g ne prend aucun argument.

Cette déclaration partielle laissait au compilateur le soin de compléter la déclaration lors de l'appel de la fonction, ou de sa définition. On perd donc un grand intérêt des prototypes. Mais à cause de l'immense quantité de code existant qui se reposait sur ce comportement, l'ANSI (puis le WG14) n'ont pas interdit de tels programmes, mais ont déclaré dès le C90 que cette construction est obsolète.

Évaluation des arguments

La norme du langage ne spécifie pas l'ordre d'évaluation des arguments. Il faut donc faire particulièrement attention aux effets de bords.

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>

int somme(int a, int b)
{return a + b;
}

int main(void)
{
    int i = 0;
    printf("%d\n",  somme(++i, i)  );
    return 0;
}

Voici un premier exemple. Lors de l'appel de la fonction somme, si l'expression ++i est évaluée avant l'expression i, alors le programme affichera 2. Si, au contraire, c'est l'expression i qui est évaluée avant l'expression ++i, alors le programme affichera 1.

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    return fonction(g(), h());
}

Dans cet autre exemple, les expressions g() et h() pouvant être évaluées dans n'importe quel ordre, on ne peut pas savoir laquelle des fonctions g et h sera appelée en premier. Si l'appel de ces fonctions provoque des effets de bord (affichage de messages, modification de variables globales...), alors le comportement du programme est imprévisible. Pour pallier à ce problème, il faut imposer l'ordre d'appel :

#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    int a,b;
    a = g();
    b = h();
    return fonction(a, b);
}

Nombre variable d'arguments

Une fonctionnalité assez utile est d'avoir une fonction avec un nombre variable d'arguments, comme la fameuse fonction printf(). Pour cela, il suffit de déclarer le prototype de la fonction de la manière suivante :

Déclaration

#include <stdarg.h>

void ma_fonction(type1 arg1, type2 arg2, ...)
{
}

Dans l'exemple ci-dessus, les points de suspension ne sont pas un abus d'écriture, mais bel et bien une notation C pour indiquer que la fonction accepte d'autres arguments. L'exemple est limité à deux arguments, mais il est bien sûr possible d'en spécifier autant qu'on veut. C'est dans l'unique but de ne pas rendre ambigüe la déclaration, qu'aucun abus d'écriture n'a été employé.

L'inclusion de l'en-tête <stdarg.h> n'est nécessaire que pour traiter les arguments à l'intérieur de la fonction. La première remarque que l'on peut faire est qu'une fonction à nombre variable d'arguments contient au moins un paramètre fixe. En effet la déclaration suivante est invalide :

Avertissement Ce code contient une erreur volontaire !
void ma_fonction(...);

Accès aux arguments

Pour accéder aux arguments situés après le dernier argument fixe, il faut utiliser certaines fonctions (ou plutôt macros) de l'en-tête <stdarg.h> :

void va_start (va_list ap, last);
type va_arg (va_list ap, type);
void va_end (va_list ap);

va_list est un type opaque dont on n'a pas à se soucier. On commence par l'initialiser avec va_start. Le paramètre last doit correspondre au nom du dernier argument fixe de la fonction, ou alors tout bon compilateur retournera au moins un avertissement.

Vient ensuite la collecte minutieuse des arguments. Il faut bien comprendre qu'à ce stade, le langage n'offre aucun moyen de savoir comment sont structurées les données (c'est à dire leur type). Il faut absolument définir une convention, laissée à l'imagination du programmeur, pour pouvoir extraire les données correctement.

Qui plus est, il faut être extrêmement vigilant lors de la récupération des paramètres, à cause de la promotion des types entiers ou réels. En effet, les entiers sont systématiquement promus en int, sauf si la taille du type est plus grande, auquel cas le type est inchangé. Pour les réels, le type float est promu en double, alors que le type long double est inchangé. C'est pourquoi ce genre d'instruction n'a aucun sens dans une fonction à nombre variable d'arguments :

Avertissement Ce code contient une erreur volontaire !
char caractere = va_arg(list, char);

Il faut obligatoirement récupérer un entier de type char, comme étant un entier de type int.

Exemple de convention

Un bon exemple de convention est la fonction printf() elle même. Elle utilise un spécificateur de format qui renseigne à la fois le nombre d'arguments qu'on s'attend à trouver mais aussi le type de chacun. D'un autre coté, analyser un spécificateur de format est relativement rébarbatif, et on n'a pas toujours besoin d'une artillerie aussi lourde.

Une autre façon de faire, relativement répandue, est de ne passer que des couples (type, objet), où type correspond à un code représentant un type (une énumération par exemple) et objet le contenu de l'objet lui-même (int, pointeur, double, etc.). On utilise alors un code spécial (généralement 0) pour indiquer la fin des arguments, ou alors un des paramètres pour indiquer combien il y en a. Un petit exemple complet :

#include <stdio.h>
#include <stdarg.h>

enum va_list{
        TYPE_FIN, TYPE_ENTIER, TYPE_REEL, TYPE_CHAINE
};

void affiche(FILE * out, ...)
{
        va_list list;
        int     type;

        va_start(list, out);

        while ((type = va_arg(list, int)))
        {
                switch (type)
                {
                case TYPE_ENTIER: fprintf(out, "%d", va_arg(list, int)); break;
                case TYPE_REEL:   fprintf(out, "%g", va_arg(list, double)); break;
                case TYPE_CHAINE: fprintf(out, "%s", va_arg(list, char *)); break;
                }
        }
        fprintf(out, "\n");
        va_end(list);
}

int main(int nb, char * argv[])
{
        affiche(stdout, TYPE_CHAINE, "Le code ascii de 'a' est ", TYPE_ENTIER, 'a', 0);
        affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, 2. * 3 / 5, 0);

        return 0;
}

L'inconvénient de ce genre d'approche est de ne pas oublier le marqueur de fin. Dans les deux cas, il faut être vigilant avec les conversions implicites, notamment dans le second cas. À noter que la conversion (transtypage) explicite des types en une taille inférieure à celle par défaut (int pour les entiers ou double pour les réels) ne permet pas de contourner la promotion implicite. Même écrit de la sorte:

affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, (float) (2. * 3 / 5), 0);

Le résultat transmis au cinquième paramètre sera quand même promu implicitement en type double.

Fonction inline

Il s'agit d'une extension ISO C99, qui à l'origine vient du C++. Ce mot clé doit se placer avant le type de retour de la fonction. Il ne s'agit que d'une indication, le compilateur peut ne pas honorer la demande, notamment si la fonction est récursive. Dans une certaine mesure, les fonctionnalités proposées par ce mot clé sont déjà prises en charge par les instructions du préprocesseur. Beaucoup préfèreront passer par une macro, essentiellement pour des raisons de compatibilité avec d'anciens compilateurs ne supportant pas ce mot clé, et quand bien même l'utilisation de macro est souvent très délicat.

Le mot clé inline permet de s'affranchir des nombreux défauts des macros, et de réellement les utiliser comme une fonction normale, c'est à dire surtout sans effets de bord. À noter qu'il est préférable de classifier les fonctions inline de manière statique. Dans le cas contraire, la fonction sera aussi déclarée comme étant accessible de l'extérieur, et donc définie comme une fonction normale.

En la déclarant static inline, un bon compilateur devrait supprimer toute trace de la fonction et seulement la mettre in extenso aux endroits où elle est utilisée. Ceci permettrait à la limite de déclarer la fonction dans un fichier en-tête, bien qu'il s'agisse d'une pratique assez rare et donc à éviter. Exemple de déclaration d'une fonction inline statique :

static inline int max(int a, int b)
{
    return (a > b) ? a : b;
}

La fonction main

Nous allons revenir ici sur la fonction main, présente dans chaque programme. Cette fonction est le point d'entrée du programme. La norme définit deux prototypes, qui sont donc portables:

int main(int argc, char * argv[]) { /* ... */ }
int main(void) { /* ... */ }

Le premier prototype est plus "général" : il permet de récupérer des paramètres au programme. Le deuxième existe pour des raisons de simplicité, quand on ne veut pas traiter ces arguments.

Si ces deux prototypes sont portables, une implémentation peut néanmoins définir un autre prototype pour main, ou spécifier une autre fonction pour le démarrage du programme. Cependant ces cas sont plus rares (et souvent spécifiques à du C embarqué).

Paramètres de ligne de commande

La fonction main prend deux paramètres qui permettent d'accéder aux paramètres passés au programme lors de son appel. Le premier, généralement appelé argc (argument count), est le nombre de paramètres qui ont été passés au programme. Le second, argv (argument vector), est la liste de ces paramètres. Les paramètres sont stockés sous forme de chaîne de caractères, argv est donc un tableau de chaînes de caractères, ou un pointeur sur un pointeur sur char. argc correspond au nombre d'éléments de ce tableau.

La première chaîne de caractères, dont l'adresse est dans argv[0], contient le nom du programme. Le premier paramètre est donc argv[1]. Le dernier élément du tableau, argv[argc], est un pointeur nul.

Valeur de retour

La fonction main retourne toujours une valeur de type entier. L'usage veut qu'on retourne 0 (ou EXIT_SUCCESS) si le programme s'est déroulé correctement, ou EXIT_FAILURE pour indiquer qu'il y a eu une erreur (Les macros EXIT_SUCCESS et EXIT_FAILURE étant définies dans l'en-tête <stdlib.h>). Il est possible par le programme appelant de récupérer ce code de retour, et de l'interpréter comme bon lui semble.

Exemple

Voici un petit programme très simple qui affiche la liste des paramètres passés au programme lorsqu'il est appelé:

#include <stdio.h>

int main(int argc, char * argv[])
{
    int i;

    for (i = 0; i < argc; i++)
        printf("paramètre %i : %s\n", i, argv[i]);

    return 0;
}

On effectue une boucle sur argv à l'aide de argc. Enregistrez-le sous le nom params.c puis compilez-le (cc params.c -o params). Vous pouvez ensuite l'appeler ainsi:

./params hello world ! # sous Unix
params.exe hello world ! # sous MS-DOS ou Windows

Vous devriez voir en sortie quelque chose comme ceci (paramètre 0 varie selon le système d'exploitation):

paramètre 0 : ./params
paramètre 1 : hello
paramètre 2 : world
paramètre 3 : !

Fonctions en C pré-ANSI

Absence de prototype lors de l'appel d'une fonction

Avant la normalisation C89, on pouvait appeler une fonction sans disposer ni de sa définition, ni de sa déclaration. Dans ce cas, la fonction était implicitement déclarée comme retournant le type int, et prenant un nombre indéterminé de paramètres.

/* Aucune déclaration de g() n'est visible. */

void f(void)
{
    g();  /* Déclaration implicite: extern int g() */
}

À cause de la grande quantité de code existant à l'époque qui reposait sur ce comportement, le C90 a conservé cette possibilité. Cependant, elle a été retirée de la norme C99, et est à éviter même lorsqu'on travaille en C90.

En effet, c'est plus qu'une bonne habitude de programmation de s'assurer que chaque fonction utilisée dans un programme ait son prototype déclaré avant qu'elle ne soit définie ou utilisée. C'est d'autant plus indispensable lorsque les fonctions sont définies et utilisées dans des fichiers différents.

Ancienne notation

À titre anecdotique, ceci est la façon « historique » de définir une fonction, avant que le prototypage ne fut utilisé. Cette notation est interdite depuis C90.

type_retour fonction(par1, par2, ..., parN)
type1 par1;
type2 par2;
...
typeN parN;
{
        /* Déclarations de variables ... */
        /* Instructions ... */
}

Au lieu de déclarer les types à l'intérieur même de la fonction, ils sont simplement décrits après la fonction et avant la première accolade ouvrante. À noter que type_retour pouvait être omis, et dans ce cas valait par défaut int.

Fichiers

Exécuter des fichiers

Les primitives sont execl, execlp, execle, exect, execv, execvp[14].

L'exemple ci-dessous lance un fichier test.exe sur un bureau de Windows 7 :

#include <unistd.h>

int main()
{
     int fichier;
     fichier = execl ( "c:\\Users\\public\\Desktop\\test.exe", "test.exe", ".", (char*)0);
}

Renommer et supprimer des fichiers

Utiliser les fonctions rename et remove[15].

Copier des fichiers

Il faut copier le contenu du premier dans le second[16].

Tableaux

Il existe deux types de tableaux : les tableaux statiques, dont la taille est connue à la compilation, et les tableaux dynamiques, dont la taille est connue à l'exécution. Nous ne nous intéresserons pour l'instant qu'aux tableaux statiques, les tableaux dynamiques seront présentés avec les pointeurs.

Syntaxe

Déclaration

T tableau[N];

Déclare un tableau de N éléments, les éléments étant de type T. N doit être un nombre connu à la compilation, N ne peut pas être une variable. Pour avoir des tableaux dynamiques, il faut passer par l'allocation mémoire.

Cette dernière restriction a été levée par la norme C99 qui a apporté le type tableau à longueur variable appelés Variable Length Array (VLA). il est maintenant possible de déclarer un tableau de type VLA par :

T tableau[expr];

où expr est une expression entière, calculée à l'exécution du programme. Ce type de tableau ne peut pas appartenir à la classe de stockage static ou extern. Sa portée est limitée au bloc dans lequel il est défini. Par rapport à un allocation dynamique, les risques de fuite mémoire sont supprimés.

Accès aux éléments

L'exemple précédent a permis de déclarer un tableau à N éléments. En C, ces éléments sont indexés de 0 à N-1, et le ie élément peut être accédé de la manière suivante (où i est supposé déclaré comme une variable entière ayant une valeur entre 0 et N-1):

tableau[i]

Note: La syntaxe suivante est équivalente à la précédente:

i[tableau]

Bien qu'autorisée par le C, elle va à l'encontre de ce à quoi nombre de programmeurs sont habitués dans d'autres langages, c'est pourquoi elle est très peu utilisée. Comme, de plus, elle n'apporte aucun réel avantage par rapport à la première syntaxe, elle est simplement à éviter, au profit de la précédente.

Exemples

int i;
int tableau[10]; /* déclare un tableau de 10 entiers */

for (i = 0; i < 10; i++) /* boucle << classique >> pour le parcours d'un tableau */
{
    tableau[i] = i;  /* chaque case du tableau reçoit son indice comme valeur */
}
Avertissement Ce code contient une erreur volontaire !
int tableau[1]; /* déclare un tableau d'un entier */

tableau[10] = 5; /* accède à l'élément d'indice 10 (qui ne devrait pas exister) */

printf("%d\n", tableau[10]);

Ce deuxième exemple, non seulement peut compiler (le compilateur peut ne pas détecter le dépassement de capacité), mais peut aussi s'exécuter et afficher le « bon » résultat. Le langage C n'impose pas à une implémentation de vérifier les accès, en écriture comme en lecture, hors des limites d'un tableau ; il précise explicitement qu'un tel code a un comportement indéfini, donc que n'importe quoi peut se passer. En l’occurrence, ce code peut très bien marcher comme on pourrait l'attendre, i.e. afficher 5, ou causer un arrêt du programme avec erreur (si la zone qui correspondrait au 11ème élément du tableau est hors de la mémoire allouée au processus, le système d'exploitation peut détecter une tentative d'accès invalide à une zone mémoire, ce qui peut se traduire par une « erreur de segmentation » qui termine le programme), ou encore corrompre une autre partie de la mémoire du processus (dans le cas où le pseudo-11ème élément correspondrait à une partie de la mémoire du processus), ce qui peut modifier son comportement ultérieur de manière très difficile à prévoir.

Ce code est donc un exemple d'un type de bogue très courant en langage C. S'il est facile à détecter dans ce code très court, il peut être très difficile à identifier dans du code plus complexe, où on peut par exemple essayer d'accéder à un indice i d'un tableau, la valeur de i pouvant varier suivant un flot d'exécution complexe, ou alors essayer de copier le contenu d'un tableau dans un autre, sans vérifier la taille du tableau de destination (ce dernier cas est connu sous le nom de débordement de tampon, dépassement de capacité, ou encore buffer overflow en anglais). Il est donc très important de s'assurer que tous les accès au contenu de tableaux, en lecture comme en écriture, se font dans les limites de ses bornes.

Tableaux à plusieurs dimensions

Les tableaux vus pour l'instant étaient des tableaux à une dimension (ie : des tableaux à un seul indice), il est possible de déclarer des tableaux possédant un nombre aussi grand que l'on veut de dimensions. Par exemple pour déclarer un tableau d'entiers à deux dimensions :

int matrice[10][5];

Pour accèder aux éléments d'un tel tableau, on utilise une notation similaire à celle vue pour les tableaux à une dimension :

matrice[0][0] = 5;

affecte la valeur 5 à la case d'indice (0,0) du tableau.

Avec le type VLA (Variable Length Array), introduit par C99, il est possible de définir un tableau à plusieurs dimensions dont les bornes ne sont connues qu'à l'exécution et non à la compilation :

static void vlaDemo(int taille1, int taille2)
{
    // Declaration du tableau VLA
    int table[taille1][taille2];

    // ...

Initialisation des tableaux

Il est possible d'initialiser directement les tableaux lors de leur déclaration :

int tableau[5] = { 1 , 5 , 45 , 3 , 9 };

initialise le tableau d'entiers avec les valeurs fournies entre accolades (tableau[0] = 1;, tableau[1] = 5;, etc.)

À noter que si on ne spécifie aucune taille entre les crochets du tableau, le compilateur la calculera automatiquement pour contenir tous les éléments. La déclaration ci-dessus aurait pu s'écrire plus simplement :

int tableau[] = { 1 , 5 , 45 , 3 , 9 };

Si on déclare un tableau de taille fixe, mais qu'on l'initialise avec moins d'éléments qu'il peut contenir, les éléments restant seront mis à zéro. On utilise cette astuce pour initialiser rapidement un tableau à zéro :

int tableau[512] = {0};

Cette technique est néanmoins à éviter, car dans ce cas le tableau sera stocké en entier dans le code du programme, faisant grossir artificiellement la taille du programme exécutable. Alors qu'en ne déclarant qu'une taille et l'initialisant à la main au début du programme, la plupart des formats d'exécutable sont capables d'optimiser en ne stockant que la taille du tableau dans le programme final.

Néanmoins, cette syntaxe est aussi utilisable pour les tableaux à plusieurs dimensions :

int matrice[2][3] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } };

Ce qui peut aussi s'écrire :

int matrice[2][3] = { 1 , 2 , 3 , 4 , 5 , 6 };

À noter que si on veut aussi utiliser l'adaptation dynamique, il faut quand même spécifier une dimension :

int identite[][3] = { { 1 , 0 , 0 } , { 0 , 1 , 0 } , { 0 , 0 , 1 } };
/* Ou plus "simplement" */
int identite[][3] = { { 1 } , { 0 , 1 } , { 0 , 0 , 1 } };

Conversion des noms de tableaux en pointeurs

A quelques exceptions près (c.f. ci-dessous), un nom de tableau apparaissant dans une expression C sera automatiquement converti en un pointeur vers son premier élément lors de l'évaluation de cette expression : si tab est le nom d'un tableau d'entiers, le nom de tableau tab sera converti dans le type pointeur vers int dans l'expression tab, et la valeur de cette expression sera l'adresse du début de la zone mémoire allouée pour le stockage des éléments du tableau, i.e. l'adresse de son premier élément. Le code suivant est par exemple valide :

int tab[10];
int *p, *q;
p = tab;
q = &(tab[0]);

La déclaration int tab[10] réserve une zone mémoire suffisante pour stocker 10 variables de type int, désignées par tab[0], tab[1]..., tab[9]. Dans l'instruction p = tab, l'expression tab est de type pointeur vers int, et la valeur affectée à p est l'adresse de la première case du tableau. Cette adresse est aussi l'adresse de la variable tab[0] : dans la seconde instruction, l'adresse de tab[0] est affectée à q via l'opérateur de prise d'adresse &. Après la seconde instruction, p et q ont la même valeur, et les instructions *p = 1, *q = 1 ou tab[0] = 1 deviennent opératoirement équivalentes.

Exceptions à la règle de conversion

Noter que dans la déclaration int tab[10], le nom tab ne désigne pas en lui-même un pointeur mais bien un tableau, de type tableau d'entiers à 10 éléments, même si ce nom est converti en pointeur dans la plupart des contextes. Les exceptions à la règle de conversion interviennent :

  • lorsque le nom du tableau est opérande de l'opérateur unaire &,
  • lorsqu'il est argument de l'opérateur sizeof,
  • lorsqu'il est argument d'un pré ou d'une post-incrémentation ou décrémentation (--,++)
  • lorsqu'il constitue la partie gauche d'une affectation,
  • lorsqu'il est suivi de l'opérateur d'accès à un champ.

Dans chaque cas, le nom du tableau gardera le sens d'une variable de type tableau, c'est-à-dire conservera son type initial :

  • dans l'expression &tab, la sous-expression tab conserve son type "tableau d'entiers à 10 éléments", et l'expression elle-même est de type "pointeur vers tableaux d'entiers à 10 éléments". Le code suivant est par exemple valide :
int tab[10];  /* tableau d'entiers à 10 éléments */
int (*p)[10]; /* pointeur vers les tableaux d'entiers à 10 éléments */
p = &tab;

Le code suivant est en revanche incorrect :

Avertissement Ce code contient plusieurs erreurs volontaires !
int tab[10];
int *p;
p = &tab; /* incorrect. '&tab' n'est pas du bon type  */
  • dans sizeof tab la taille calculée sera celle de tab considéré encore comme une variable de type "tableau d'entiers à 10 éléments", soit 10 fois la taille d'un int.
  • dans tab++ , tab-- , --tab ou ++tab , la sous-expression tab n'est ni une expression numérique ni un pointeur, mais bien une variable de type tableau : elle n'est donc ni incrémentable, ni décrémentable, et l'expression globale ne sera jamais compilable,
  • dans l'instruction tab = expr où expr est une expression quelconque, tab est toujours considéré comme une variable de type "tableau d'entiers à 10 éléments"... la règle de conversion des noms de tableaux et les restrictions sur les conversion explicites font qu'il est impossible que expr soit de même type : cette affectation générera toujours une erreur de typage, et ne sera jamais compilable,
  • dans l'expression tab.champ où champ est un nom de champ quelconque, tab est de type tableau, i.e. ne possède aucun champ, donc cette expression ne sera jamais compilable.

Le cas où tab est membre gauche d'une affectation montre que les valeurs de type tableau ne sont jamais manipulables de manière directe : il est par exemple impossible d'affecter un tableau à un autre tableau - l'instruction tab_1 = tab_2 est toujours incompilable, à cause de la conversion en pointeur de tab_2 - ou de comparer structurellement deux tableaux par une comparaison simple - dans tab_1 == tab_2 , les deux noms seront convertis en pointeur, et la comparaison effectuée sera celle des adresses.

Cas des paramètres de fonctions

Tout paramètre de fonction déclaré comme étant de type "tableau d’éléments de type T" est automatiquement converti en un pointeur vers T à la compilation. Les deux écritures suivantes sont équivalentes :

version avec tableaux version avec pointeurs
void f(int t[])
{
   /* ... */
}
void g(int m[][10])
{
   /* ... */
}

void f(int *t)
{
   /* ... */
}
void g(int (*m)[10])
{
   /* ... */
}

Dans la première version de f et g , les paramètres t et m sont bien des pointeurs, conformément à l'équivalence avec la seconde version, et sont donc réaffectables dans le corps de ces fonctions. La taille en la première dimension de t et de m , même si elle est spécifiée dans le code (e.g. void g(int m[10][10]) ), sera de toute manière effacée à la compilation : il ne s'agit alors que d'un simple commentaire pour le programmeur.

Cette équivalence est cohérente avec la règle de conversion des noms de tableaux : lors d'un appel de la forme f(tab) où tab est un tableau d'entiers, le nom tab sera converti en pointeur vers int , qui est bien le type du paramètre de f dans la seconde version.

Cas des tableaux externes

Dans le cas des déclarations externes, et contrairement au cas des paramètres de fonctions, il n'y a pas équivalence entre la notation par tableaux et la notation par pointeurs. Le programme suivant est en général compilable, mais incorrect :

Avertissement Ce code contient une erreur volontaire !
fichier1.c fichier2.c
#include <stdio.h>
int tab[] = {42};

void f(void)
{
  printf("fichier1 : tab = %p\n", tab);
}

#include <stdio.h>
extern int *tab;
void f(void)

int main(int nb, char *argv[] )
{
    f();
    printf("fichier2 : tab = %p\n", tab);
    return 0;
}

Les valeurs affichées pour tab dans main et dans f seront en général distinctes. L'erreur se situe dans la déclaration externe de tab située dans le fichier fichier2.c. Il aurait fallu écrire la déclaration suivante, spécifiant correctement le type de tab dans fichier2.c comme étant celui d'un tableau et non d'un pointeur (mais ne spécifiant pas sa taille, le compilateur n'effectuant de toute manière aucun test de dépassement) :

extern int tab[];

L'absence de message d'erreur dans la compilation de la première version de ce programme n'est due qu'à l'insuffisance de la vérification de la cohérence globale du typage lors de la phase de liaison : il est plus généralement possible de déclarer une variable externe de n'importe quel autre type que son type réel, sans que l'incohérence globale du typage soit nécessairement signalée par le compilateur. Aucune norme ne spécifiant le comportement du programme résultant, ce comportement est, de fait, indéfini.

Tableaux VLA passés à une fonction

En C99, il est possible de passer à une fonction un tableau de type VLA (Variable Length Array). Il faut passer les tailles des dimensions en premier.

Exemple de fonction recevant en argument un tableau de type VLA à deux dimensions :

static void vlaPrint(int taille1, int taille2, int table[taille1][taille2])
{
    // Ecriture du tableau sur stdout
    for (int i=0; i< taille1; i++)
    {
        for (int j=0; j< taille2; j++)
        {
            (void)printf("table[%d][%d] = %d\n", i, j, table[i][j]);
        }
    }
}

Chaînes de caractères

Comme il a été dit tout au long de cet ouvrage, les chaînes de caractères sont des tableaux particuliers. En déclarant une chaîne de caractères on peut soit la manipuler en tant que pointeur soit en tant que tableau. Considérez les déclarations suivantes :

char * chaine1 = "Ceci est une chaine";
char chaine2[] = "Ceci est une autre chaine";

Bien que se manipulant exactement de la même façon, les opérations permises sur les deux variables ne sont pas tout à fait les mêmes. Dans le premier cas on déclare un pointeur sur une chaîne de caractères statique, dans le second cas, un tableau (alloué soit sur la pile si la variable est déclarée dans une fonction ou soit dans le segment global si la variable est globale) de taille suffisante pour contenir tous les caractères de la chaîne affectée (incluant le caractère nul).

Le second cas est donc une notation abrégée pour char chaine2[] = { 'C', 'e', 'c', 'i', ' ', ..., 'e', '\0' };. Dans tous les autres cas (ceux où une chaîne de caractères ne sert pas à initialiser un tableau), la chaîne déclarée est statique (les données sont persistantes entre les différents appels de fonctions), et sur certaines architectures, pour ne pas dire toutes, elle est même en lecture seule. En effet l'instruction chaine1[0] = 'c'; peut provoquer un accès illégal à la mémoire. En fait le compilateur peut optimiser la gestion des chaînes en regroupant celles qui sont identiques. C'est pourquoi il est préférable de classifier les pointeurs sur chaîne de caractères avec le mot clé const.

On comprend aisément que si chaine2 est alloué sur la pile (déclaré dans une fonction), la valeur ne pourra pas être utilisée comme valeur de retour. Tandis que la valeur de chaine1 pourra être retournée même si c'est une variable locale.

Pointeurs

Dans cette section, nous allons présenter un mécanisme permettant de manipuler les adresses, les pointeurs. Un pointeur a pour valeur l'adresse d'un objet C d'un type donné (un pointeur est typé). Ainsi, un pointeur contenant l'adresse d'un entier sera de type pointeur vers entier.

Usage et déclaration

L'opérateur & permet de connaitre l'adresse d'une variable, on dira aussi la référence. Toute déclaration de variable occupe un certain espace dans la mémoire de l'ordinateur. La référence permet de savoir où cet emplacement se trouve. En simplifiant à l'extrême, on peut considérer la mémoire d'un ordinateur comme une gigantesque table d'octets. Quand on déclare une variable de type int, elle sera allouée à un certain emplacement (ou dit autrement : un indice, une adresse ou une référence) dans cette table. Un pointeur permet simplement de stocker une référence, il peut donc être vu comme un nombre allant de 0 à la quantité maximale de mémoire dont dispose votre ordinateur (moins un, pour être exact).

Un pointeur occupera habituellement toujours la même taille (occupera la même place en mémoire), quelque soit l'objet se trouvant à cet emplacement. Il s'agit en général de la plus grande taille directement gérable par le processeur : sur une architecture 32bits, elle sera de 4 octets, sur une architecture 64bits, 8 octets, etc. Le type du pointeur ne sert qu'à renseigner comment sont organisées les données suivant l'adresse référencée par le pointeur. Ce code, par exemple, affiche la référence d'une variable au format hexadécimal :

int i;

printf("%p\n", &i);

Pouvoir récupérer l'adresse n'a d'intérêt que si on peut manipuler l'objet pointé. Pour cela, il est nécessaire de pouvoir déclarer des pointeurs, ou dit autrement un objet pouvant contenir des références. Pour cela on utilise l'étoile (*) entre le type et le nom de la variable pour indiquer qu'il s'agit d'un pointeur :

T * pointeur, * pointeur2, /* ..., */ * pointeurN;

Déclare les variables pointeur, pointeur2, ..., pointeurN de type pointeur vers le type T. À noter la bizarrerie du langage à vouloir associer l'étoile à la variable et non au type, qui oblige à répéter l'étoile pour chaque variable.

/* Ce code contient une déclaration volontairement confuse */
int * pointeur, variable;

Cet exemple de code déclare un pointeur sur un entier de type int et une variable de type int. Dans un vrai programme, il est rarement possible d'utiliser des noms aussi triviaux, aussi il est recommandé de séparer la déclaration des variables de celles des pointeurs (ou d'utiliser l'instruction typedef, qui, elle, permet d'associer l'étoile au type), la lisibilité du programme sera légèrement améliorée.

Il est essentiel de bien comprendre ce qui a été déclaré dans ces exemples. Chaque pointeur peut contenir une référence sur un emplacement de la mémoire (un indice dans notre fameuse table). On peut obtenir une référence (ou un indice) avec l'opérateur & (ou allouer une référence soi-même avec des fonctions dédiées, c.f la section suivante). Cet opérateur transforme donc une variable de type T en un pointeur de type T *. Insistons sur le terme variable, car évidemment des expressions telles que '&23435' ou '&(2 * a / 3.)' n'ont aucun sens, dans la mesure où les constantes et expressions du langage n'occupent aucun emplacement susceptible d'intéresser votre programme.

Ce code, par exemple, affiche la référence d'une variable dans un format défini par l'implémentation (qui peut être hexadécimal, ou une combinaison "segment:offset", par exemple) :

int i;

printf("%p\n", &i);

Il ne faut pas oublier que, comme toutes les variables locales en C, un pointeur est à l'origine non initialisé. Une bonne attitude de programmation est de s'assurer que lorsqu'il ne pointe pas vers un objet valide, sa valeur est mise à zéro (ou NULL, qui est déclaré entre autre dans <stdio.h>).

L'arithmétique des pointeurs

L'arithmétique associée aux pointeurs est sans doute ce qui a valu au C sa réputation « d'assembleur plus compliqué et plus lent que l'assembleur ». On peut très vite construire des expressions incompréhensibles avec les opérateurs disponibles. Dans la mesure du possible, il est conseillé de se limiter à des expressions simples, quitte à les décomposer, car la plupart des compilateurs savent très bien optimiser un code C.

Déréférencement

Il s'agit de l'opération la plus simple sur les pointeurs. Comme son nom l'indique, il s'agit de l'opération réciproque au référencement (&). L'opérateur associé est l'étoile (*), qui est aussi utilisé pour déclarer un type pointeur. Cet opérateur permet donc de transformer un pointeur de type T *, en un objet de type T, les opérations affectant l'objet pointé :

int variable = 10;
int * pointeur = &variable;

*pointeur = 20; /* Positionne 'variable' à 20 */

Ici, pointeur contient une adresse valide, celle de variable ; son déréférencement est donc possible. Par contre, si pointeur était une variable locale non initialisée, son déréférencement provoquerait à coup sûr un arrêt brutal de votre programme.

Vous obtiendrez le même résultat, si pointeur est initialisé à NULL. Cette adresse est invalide et toute tentative de déréférencement se soldera par un arrêt du programme.

Arithmétique de base

L'arithmétique des pointeurs s'apparente à celle des entiers, mais il est important de comprendre la distinction entre ces deux concepts.

Les opérations arithmétiques permises avec les pointeurs sont :

  • Addition / soustraction d'une valeur entière à un pointeur (on avance / recule d'un nombre de cases mémoires égal à la taille du type T) : le résultat est donc un pointeur, de même type que le pointeur de départ. Il faut bien faire attention avec ce genre d'opération à ne pas sortir du bloc mémoire, car le C n'effectuera aucun test pour vous. Considérez l'exemple suivant :
    /* Parcours les éléments d'un tableau */
int tableau[N];
int * p;

for (p = tableau; p < &tableau[N]; p ++)
{
     /* ... */
}

    Normalement un tableau de N cases permet d'être itéré sur les indices allant de 0 à N - 1, inclusivement. L'expression &tableau[N] fait référence la case mémoire non allouée immédiatement après le plus grand indice, donc potentiellement source de problème. Toutefois, par exception pour le premier indice après le plus grand, C garantit que le résultat de l'expression soit bien défini. Bien sûr, il ne faut pas déréférencer ce pointeur.

    À noter qu'à l'issue de la boucle, p pointera sur la N+1ème case du tableau, donc hors de l'espace alloué. Le C autorise tout à fait ce genre de pratique, il faut juste faire attention à ne pas déréférencer le pointeur à cet endroit.

  • Soustraction de deux pointeurs de même type (combien d'objet de type T y a t-il entre les deux pointeurs) : le résultat est donc un entier, de type ptrdiff_t.
    Avertissement Ce code contient une erreur volontaire !
    int       autre_tableau[3];
int       tableau[10];
int *     p     = &tableau[5];  /* p pointe sur le 6e élément du tableau */
int *     q     = &tableau[3];  /* q pointe sur le 4e élément du tableau */
ptrdiff_t diff1 = p - q;        /* diff1 vaut 2 */
ptrdiff_t diff2 = q - p;        /* diff2 vaut -2 */

q = &autre_tableau[2];
ptrdiff_t dif3 = p - q; /* Erreur ! */

    Dans cet exemple, les deux premières soustractions sont définies, car p et q pointent sur des éléments du même tableau. La troisième soustraction est indéfinie, car on utilise des adresses d'éléments de tableaux différents.

    Notons que l'opérateur [] s'applique toujours à une opérande de type entier et une autre de type pointeur. Lorsqu'on écrit tableau[i], il y a en fait une conversion de tableau à pointeur avec l'application de l'opérateur []. On peut donc bien sûr utiliser l'opérateur [] avec un pointeur pour opérande :

    int   a;
int b;
int * p = &a; /* On peut accéder à la valeur de 'a' via 'p[0]' ou '*p' */

/* p[1] est indéfini - n'espérez pas accéder à la valeur de b depuis l'adresse de a */

Arithmétique avec effet de bord

C'est sans doute ce qui a donné des sueurs froides à des générations de programmeurs découvrant le C : un usage « optimisé » de la priorité des opérateurs, le tout imbriqué dans des expressions à rallonge. Par exemple 'while( *d++ = *s++ );', pour copier une chaine de caractères.

En fait, en décomposant l'instruction, c'est nettement plus simple qu'il ne parait. Par exemple :

int i;
int * entier;

/* ... */

i = *entier++; /* i = *(entier++); */

Dans ce cas de figure, l'opérateur d'incrémentation ayant priorité sur celui de déréférencement, c'est celui-ci qui sera appliqué en premier. Comme il est postfixé, l'opérateur ne prendra effet qu'à la fin de l'expression (donc de l'affectation). La variable i sera donc tout simplement affectée de la valeur pointée par entier et après cela le pointeur sera incrémenté. Voici les différents effets suivant les combinaisons de ces deux opérateurs :

i = *++entier; /* Incrémente d'abord le pointeur, puis déréférence la nouvelle adresse pointée */
i = ++*entier; /* Incrémente la valeur pointée par "entier", puis affecte le résultat à "i" */
i = (*entier)++; /* Affecte la valeur pointée par "entier" et incrémente cette valeur */

On peut évidemment complexifier les expressions à outrance, mais privilégier la compacité au détriment de la clarté et de la simplicité dans un hypothétique espoir d'optimisation est une erreur de débutant à éviter.

Le pointeur void *

Ce pointeur est un cas particulier. Il permet de pointer sur un type quelconque. Il est notemment utilisé dans la fonction malloc():

void * malloc(int n);

En pratique, il faut penser à transformer ce pointeur pour qu'il devienne utilisable, même si certains compilateurs acceptent de l'utiliser directement:

// Allocation avec conversion
int * p; // Pointeur p sur le type int
p = (int *) malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
*p = 4; // Modification

// Allocation sans conversion
int * p; // Pointeur p sur le type int
p = malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
*p = 4; // Modification

Tableaux dynamiques

Un des intérêts des pointeurs et de l'allocation dynamique est de permettre de décider de la taille d'une variable au moment de l'exécution, comme par exemple pour les tableaux. Ainsi pour allouer un tableau de n entiers (n étant connu à l'exécution), on déclare une variable de type pointeur sur entier à laquelle on alloue une zone mémoire correspondant à n entiers :

int * alloue_tableau(int n, size_t taille)
{
    return malloc(n * taille);
}

/* Ailleurs dans le programme */
int * tableau = alloue_tableau(256, sizeof *tableau);
if (tableau  != NULL)
{
    /* opérations sur le tableau */
    /* ... */
    free( tableau );
}

Cet exemple alloue un tableau de 256 cases. Bien que la variable soit un pointeur, il est dans ce cas permis d'accéder aux cases de 0 à 255, soit entre les adresses &tableau[0] et &tableau[255], incluses.

Tableaux dynamiques à deux dimensions

Tout comme on pouvait allouer des tableaux statiques à plusieurs dimensions, on peut allouer des tableaux dynamiques à plusieurs dimensions. Pour ce faire, on commence là-aussi par déclarer un pointeurs approprié : un pointeur sur des pointeurs (etc.) sur des types. Pour déclarer un tableau dynamique d'entiers à deux dimensions :

int ** matrice;

L'allocation d'un tel objet va se dérouler en plusieurs étapes (une par étoile), on alloue d'abord l'espace pour un tableau de pointeurs vers entier. Ensuite, on alloue pour chacun de ces tableaux l'espace pour un tableau d'entiers. Si on veut une matrice 4x5 :

#define LIGNES 4
#define COLONNES 5
int i;
int ** matrice = malloc(sizeof *matrice * LIGNES);

for (i = 0; i < LIGNES; i++)
{
        matrice[i] = malloc(sizeof **matrice * COLONNES);
}

Il ne faut jamais oublier de libérer la mémoire allouée précédemment. Ainsi, à tout appel de malloc doit correspondre un appel de free Pour libérer l'espace alloué ci-dessus, on procède de manière inverse, en commençant par libérer chacune des lignes du tableau, puis le tableau lui même :

for(i = 0; i < LIGNES; i++)
{
        free(matrice[i]);
}
free(matrice);

Tableaux dynamiques à trois dimensions

Voici maintenant un exemple d'une fonction créant un tableau à trois dimensions avec surtout, et c'est très important, les tests des valeurs de retour des fonctions malloc :

int *** malloc_3d(int nb_tableau, int lignes, int colonnes)
{
    int i;
    int j;

    int***t = malloc(sizeof(*t) * nb_tableau);
    /* première dimension */
    if (t==NULL)
        {
            printf ("Impossible d'initialiser avec malloc\n" );
            exit (-1);
        }

    for (i=0;i< nb_tableau;i++) {
        t[i] = malloc(sizeof(**t) * lignes);
        /* deuxième dimension */
        if (t[i]==NULL) {
            printf ("Impossible d'initialiser avec malloc\n" );
            exit (-1);
        }
        for (j=0;j<lignes;j++) {
            /* troisième dimension */
            t[i][j] = malloc(sizeof(***t) * colonnes);
            if (t[i][j]==NULL)
                {
                    printf ("Impossible d'initialiser avec malloc\n" );
                    exit (-1);
            }
        }
    }
    return t;
}

Utilisation des pointeurs sur des tableaux particuliers

Il est possible avec un pointeur de lire/parcourir les éléments d'une structure. Chaque éléments d'une structure utilise un espace qui permet de calculer des déplacements.

 #include <stdio.h>
 #include <string.h>
 struct s{
  int a;
  int b;
  char *s;
 }s;
 int
 main(void)
 {
  struct s st={1,1,"salut"};
  void *p=&st;
  printf ("%s\n",
            (char *)((void **)p)[2]);
  /*<==OU==>*/
  p += 2*sizeof(int);
  printf ("%s\n",
            (char *)((void **)p)[0]);
  /*<=DE LA MEME MANIERE=>*/
  p -= 2*sizeof(int);
  /*====================================*/
  memset (p,-1,
          2*sizeof(int));
  printf ("%i,%i\n",
           st.a, st.b);
  memset ((int *)p,0,
          2*sizeof(int));
  printf ("%i,%i\n",
           st.a, st.b);
  ((int *)p)[0] = 1;
  ((int *)p)[1] = 0;
  printf ("%i,%i\n",
           st.a, st.b);
  printf ("%s\n",st.s);
  return 0;
 }

Vous trouverez un exemple complet sur les tableaux et les pointeur ici[1]

Utilisation des pointeurs pour passer des paramètres par adresse

Toutes les variables en C, à l'exception des tableaux, sont passés par valeurs aux paramètres des fonctions. C'est à dire qu'une copie est effectuée sur la pile d'appel. Si bien que toutes les modifications de la variable effectuées dans la fonction seront perdues une fois de retour à l'appelant. Or, il y a des cas où l'on aimerait bien pouvoir modifier une variable passée en paramètre et que ces modifications perdurent dans la fonction appelante. C'est un des usages des paramètres par adresse : permettre la modification d'une variable de l'appelant, comme dans l'exemple suivant :

#include <stdio.h>
/* Ce code échange le contenu de deux variables */
void echange(int * a, int *b)
{
        int tmp=0,
                  *t[3]={&tmp,a,b};
        unsigned short int i;
        for (i=0;i<3;i++)
             *t[i] = *t[(i!=2)*(i+1)];
        /*tmp = *a;  <=> *t[0]=*t[1] */
        /**a = *b;  <=>  *t[1]=*t[2] */
        /**b = tmp; <=>  *t[2]=*t[0] */
}

int main(void)
{
    int a = 5;
    int b = 2;

    printf("a = %d, b = %d.\n", a, b);

    /* On passe à 'echange' les adresses de a et b. */
    echange(&a, &b);

    printf("a = %d, b = %d.\n", a, b);

    echange(&a, &b);

    printf("a = %d, b = %d.\n", a, b);

    return 0;
}

Ce passage par adresse est extrêmement répandu pour optimiser la quantité de données qui doit transiter sur la pile d'appel (qui est, sur beaucoup de systèmes, de taille fixe). En fait, même si la variable ne doit pas être modifiée, on utilise quand même un passage par adresse, juste pour éviter la copie implicite des variables autres que les tableaux. Ceci est particulièrement intéressant avec les structures, puisque celles-ci ont tendance à être assez imposantes, et cela ne nuit pas trop la lisibilité du programme.

Tableaux dynamiques et passage de tableaux comme arguments d'une fonction

Comme on vient de le voir, l'intérêt principal d'une allocation dynamique est de pouvoir lancer son programme sans connaître la taille du tableau qu'on utilisera; celle-ci sera établie en cours de fonctionnement. Cependant, un autre intérêt du tableau dynamique est de pouvoir être passé comme argument d'une fonction pour des tableaux à plusieurs dimensions.

En reprenant la fonction malloc_3d() vue précédemment, on peut écrire :

int fonction_3d (int ***tab);

int main(int argc, char **argv)
{
   /* Tableau dynamique créé avec des malloc() */
   int ***tab = malloc_3d(2, 10, 10);

   fonction_3d (tab);

    return EXIT_SUCCESS;
}
/* fonction recevant le tableau en 3d */
void fonction_3d (int ***tab)
{
  /* On peut utiliser ici la notation tab[i][j][k] en veillant à
     ce que i, j, k ne sortent pas des bornes du tableau -> dans
     cet exemple tab[3][9][9] est illégal alors que tab[1][3][7]
     peut être utilisé.
     On ne pourra pas savoir si on sort du tableau !
  */
}

Pointeurs vers fonctions

Les pointeurs vers les fonctions sont un peu spéciaux, parce qu'ils n'ont pas d'arithmétique associée (car une telle arithmétique n'aurait pas beaucoup de sens). Les opérations permises avec les pointeurs sur fonctions sont en fait relativement limitées :

type_retour (*pointeur_fonction)(liste_paramètres);

Déclare pointeur_fonction, un pointeur vers une fonction prenant liste_paramètres comme paramètres et renvoyant type_retour. Le parenthésage est ici obligatoire, sans quoi l'étoile se rattacherait au type de retour. Pour faire pointer un pointeur vers une fonction, on utilise une affectation « normale » :

pointeur_fonction = &fonction;
/* Qui est en fait équivalent à : */
pointeur_fonction = fonction;

Où fonction est compatible avec le pointeur (mêmes paramètres et valeur de retour). Une fois que le pointeur pointe vers une fonction, on peut appeler cette fonction :

(*pointeur_fonction)(paramètres);
/* Ou plus simplement, mais moins logique syntaxiquement */
pointeur_fonction(paramètres);

Exemple simple d'utilisation de pointeur de fonction avec retour

On fait une comparaison de deux entiers (5 et 4) via un pointeur sur une fonction de comparaison.

#include <stdio.h>
#include <stdlib.h>

int comparaison_plus_grand_que(int a, int b)
{
    return a > b ;
}

int main(int argc, char * argv[])
{
    int taille = 10;
    int (*p_comparaison)(int,int); // pointeur de fonction
    int a = 5, b = 4;

    p_comparaison = comparaison_plus_grand_que;
    if((*p_comparaison)(a,b)) // appel de la fonction via le pointeur
        printf("%d est plus grand que %d\n", a ,b);
    else
        printf("%d est plus petit que %d\n", a ,b);

    return EXIT_SUCCESS;
}

Pointeurs de fonctions

Exemple sans pointeur

On écrit une fonction graphique.

Cette fonction dessine f.

//INITIALISATION :
f est une FORME GRAPHIQUE
f := "_/-\_"
//.
//DECLARATION DE LA FONCTION dessine_f :
fonction dessine_f()
 dessine f
fin de fonction

Cela donne le résultat :

_/-\_

Avec la fonction f qui est fixe, dessine_f() ne sait dessiner que la forme "_/-\_".

Si on remplace la valeur intrinsèque de f par un pointeur de fonction, la fonction pourra dessiner n'importe quelle forme.

Même exemple en utilisant un pointeur de fonction
En C, le nom d'une fonction est un pointeur. On peut l'utiliser comme argument :
//INITIALISATION :
g, h sont des FORMES GRAPHIQUES
g := "/-\/-\"
h := "--\/--"
//.
//DECLARATION DE LA FONCTION dessine_f :
fonction dessine_f(P_f)
 dessine P_f()
fin de fonction
//.
dessine_f(g)  donnera :"/-\/-\"
dessine_f(h)  donnera :"--\/--"

Voir Exemple graphique (avec Gnuplot) ci-dessous.

Exemple numérique

  • Testé sous Code Block (Windows,Linux).
  • Passer deux pointeurs de fonctions à une fonction.

Code source

  • Ici on passe les deux fonctions f et g à la fonction f1_o_f2().
  • La même fonction peut calculer gof, fog et fof...
  • On peut remarquer que les pointeurs de fonctions ont les mêmes types arguments que les fonctions qu'ils vont recevoir.

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* ------ Fonction f ------------ */
double f(double x){return( pow(x,2.));}
/* ------------------------------ */
char  feq[] = "x**2";
/* ------------------------------ */

/* ------ Fonction g ------------ */
double g(double x){return(2.0*x + 3.0);}
/* ------------------------------ */
char  geq[] = "2.0*x + 3.0";
/* ------------------------------ */

/* -Fonction fog  (g suivie de f)-*/
double f1_o_f2(
double (*P_f1)(double x),/* Pointeur pour la première fonction */
double (*P_f2)(double x),/* Pointeur pour la deuxième fonction */
double a
)
{
 return((*P_f1)( ((*P_f2)(a))) );
}
/* ------------------------------ */

/* ------------------------------ */
int main(void)
{
double a = 2.0;

 printf(" f : x-> %s\n", feq);
 printf(" g : x-> %s\n", geq);
 printf(" \n\n");

   printf(" f(g(%.0f)) = %6.1f\n", a, f1_o_f2(f,g,a));
   printf(" g(f(%.0f)) = %6.1f\n", a, f1_o_f2(g,f,a));
   printf(" f(f(%.0f)) = %6.1f\n", a, f1_o_f2(f,f,a));

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Résultat ;

f : x-> x**2
g : x-> 2.0*x + 3.0

f(g(2)) = 49.0
g(f(2)) = 11.0
f(f(2)) = 16.0

Press return to continue.

Exemple graphique (avec Gnuplot)

  • Testé sous Code Block (Windows,Linux).
  • Passer un pointeurs de fonctions à une fonction.
  • Vous pourrez approfondir ce travail en analyse avec le cours de Wikiversité: Mathc Home Edition.

Code source

  • La fonction Gplt() dessine f(x) et g(x)...
  • On peut remarquer que les pointeurs de fonctions ont les mêmes types arguments que les fonctions qu'ils vont recevoir.
/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* --- Dessinons f et g --------- */
double f(double x){return(  pow(x,2.));}
double g(double x){return(2.0*x + 3.0);}
/* ------------------------------ */

/* Le fichier de points: [a,f(a)] */
void Gplt(
double (*P_f)(double x)
)
{
FILE   *fp;
double   a;

        fp = fopen("data","w");
   for(a = -5.0; a <= 5.0; a += 0.3)
       fprintf(fp," %6.3f   %6.3f\n",
                    a,      ((*P_f)(a)) );
 fclose(fp);
}

/* ------------------------------ */
int main(void)
{

 printf("f) Dans gnuplot -> plot \"data\" ");
    Gplt(f);
 getchar();

 printf("g) Dans gnuplot -> plot \"data\" ");
    Gplt(g);

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Tableau de pointeurs de fonctions

Premier exemple

Nous avons des fonctions semblables. Nous voulons les associer pour pouvoir les manipuler dans des boucles.

Nous allons créer un tableau de pointeurs de fonctions.

Je vais utiliser les fonctions trigonométriques prédéfinies pour faciliter la lecture.

Déclaration d'un tableau de pointeurs de fonctions

double (*TrigF[6])(double x) = {cos,sin,tan,atan,asin,acos};
  • Toutes les fonctions ont la même forme.
  • double FUNCTION(double)
  • Le tableau à la même forme que les fonctions.
  • double ARRAY(double)
  • Il y a six fonctions. (0,1,2,3,4,5)= {cos,sin,tan,atan,asin,acos}.

Exemple d'un appel

   cos(.5) = TrigF[0](.5)

Exemple à tester

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
double (*TrigF[6])(double x) = {cos,sin,tan,atan,asin,acos};

double x= .5;
int    i=  0;

 printf(" Nous avons declare un tableau "\
        " de pointeurs de fonctions.\n  "\
        " J'ai utilise ici les fonctions predefinie du c.\n");

  printf("       cos(%.1f)  = %.3f  \n",  x,     cos(x));
  printf(" TrigF[%d](%.1f)) = %.3f\n\n",i,x,TrigF[i](x));

 printf(" Press return to continue");
 getchar();

 return 0;
}

Application

  • Créer un tableau de valeurs des fonctions trigonométriques.
  • Imprimer le résultat dans cette ordre (sin,cos,tan,acos,asin,atan)
  • Pour .1 <= x <+ .5
  • Ce travail pourrait être récupéré dans une matrice.

Résultat dans un fichier

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
FILE   *fp = fopen("list.txt","w");

double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

int    i= 6;
double x= .1;

fprintf(fp,"   x || sin    cos    tan    acos   asin   atan \n");

     for(;x<=.5;x+=.1)
        {
         fprintf(fp," %.1f ||",x);
         for(i=6;i;)
            fprintf(fp," %.3f ",TrigF[--i](x));
         fprintf(fp,"\n");
        }

 fclose(fp);

 printf("\n\n Ouvrir le fichier list.txt\n");
 getchar();

 return 0;
}

Le résultat :

  x || sin    cos    tan    acos   asin   atan
0.1 || 0.100  0.995  0.100  1.471  0.100  0.100
0.2 || 0.199  0.980  0.203  1.369  0.201  0.197
0.3 || 0.296  0.955  0.309  1.266  0.305  0.291
0.4 || 0.389  0.921  0.423  1.159  0.412  0.381
0.5 || 0.479  0.878  0.546  1.047  0.524  0.464

Remarque :
* Attention à l'ordre des fonctions dans la déclaration du tableau.
* double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

Au démarage :
* La décrémentation ce fait dans le tableau. TrigF[--i](x)
* Il entre 6 dans le tableau.
* 6 est décrémenté -> 5 (avant l'appel de la fonction --i)
* La sixième fonctions est appelé (Sin).
* La numéro cinq. :)

Au final :
* Il entre UN dans le tableau.
* UN est décrémenté -> 0
* La première fonctions est appelé (atan).
* La numéro zéro. :))

* i est égal à zéro en rentrant dans la boucle.
* Le cycle est cassé. :(

Avec résultat à l'écran

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

int    i= 6;
double x= .1;

     for(;x<=.5;x+=.1)
       {
         printf("\n");
         for(i=6;i;) printf(" %.3f ",TrigF[--i](x));
       }

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Deuxième exemple

Nous voulons créer la fonction Derivate capable de calculer la dérivé première et seconde d'une fonction, en utilisant un tableau de pointeurs de fonctions.

Déclaration d'un tableau de pointeurs de fonctions

  • Voir listing en fin de page.
double (*Derivate[3])(double (*P_f)(double x),double a,double h) = {fx,Dx_1,Dx_2};

  • Toutes les fonctions (fx,Dx_1,Dx_2) ont la même forme.
  • double FUNCTION(double (*P_f)(double x) double double)
  • Le tableau à la même forme que les fonctions.
  • double ARRAY(double (*P_f)(double x) double double)

  • Il y a trois fonctions. (0,1,2)= {fx, Dx_1, Dx_2}.

  • La fonction fx donne f.
  • Supprimer cette fonction et travailler sur deux fonctions.
  • Réfléchissez.

Exemple d'un appel

     f(x)=Derivate[0](f,x,0.)

  • Derivate[0] donne f(x).
  • Voir la fonction fx() la première fonction du tableau.
  • h = 0 dans cet appel parce qu'il n'est pas utilisé (voir code de fx())

Exemple à tester

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* ------ Fonction f ------------ */
double f(double x){return( pow(x,2.));}
/* ------------------------------ */
char  feq[] = "x**2";
/* ------------------------------ */

/* ------ Fonction g ------------ */
double g(double x){return(
 pow(cos(x),2.)+sin(x)+x-3);}
/* ------------------------------ */
char  geq[] = "cos(x)**2+sin(x)+x-3";
/* ------------------------------ */

/* ----------------------------- */
double fx(
double (*P_f)(double x),
double a,
double h
)
{
 return( ((*P_f)(a)) );
}

/* ------------------------------
 f'(a) = f(a+h) - f(a-h)
          -------------
              2h
   ------------------------------ */
double Dx_1(
double (*P_f)(double x),
double a,
double h
)
{
 return( ( ((*P_f)(a+h))-((*P_f)(a-h)) ) / (2.*h) );
}

/* -----------------------------
 f''(a) = f(a+h) - 2 f(a) + f(a-h)
           ----------------------
                     h**2
   ------------------------------- */
double Dx_2(
double (*P_f)(double x),
double a,
double h
)
{
 return( (((*P_f)(a+h))-2*((*P_f)(a))+((*P_f)(a-h))) / (h*h) );
}

/* ------------------------------ */
int main(void)
{
double (*Derivate[3])(double (*P_f)(double x),
                      double a,
                      double h) = {fx,Dx_1,Dx_2};
double x = 2;
double h = 0.001;

printf("\n\n");

printf("  f(%.3f) = %.3f = %.3f \n",x,f(x),       Derivate[0](f,x,0.));
printf(" f'(%.3f) = %.3f = %.3f \n",x,Dx_1(f,x,h),Derivate[1](f,x,h));
printf("f''(%.3f) = %.3f = %.3f \n",x,Dx_2(f,x,h),Derivate[2](f,x,h));

printf("\n\n");

printf("  g(%.3f) = %.3f = %.3f \n",x,g(x),       Derivate[0](g,x,0.));
printf(" g'(%.3f) = %.3f = %.3f \n",x,Dx_1(g,x,h),Derivate[1](g,x,h));
printf("g''(%.3f) = %.3f = %.3f \n",x,Dx_2(g,x,h),Derivate[2](g,x,h));

    printf("\n\n Press return to continue.");

 getchar();

 return 0;
}

Types avancés - structures, unions, énumérations

Structures

struct ma_structure {
    type1 champ1;
    type2 champ2;
...
    typeN champN;
} var1, var2, ..., varM;

Déclare une structure (ou enregistrement) ma_structure composé de N champs, champ1 de type type1, champ2 de type type2, etc. On déclare, par la même occasion, M variables de type struct ma_structure.

Accès aux champs

L'accès aux champs d'une structure se fait avec un point :

struct complexe {
    int reel;
    int imaginaire;
} c;

c.reel = 1;
c.imaginaire = 2;

Initialisation

Il y a plusieurs façons d'initialiser une variable de type structure :

  • En initialisant les champs un à un :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable;
variable.ch = 'a';
variable.nb = 12345;
variable.pi = 3.141592;

    Cette façon est néanmoins pénible lorsqu'il y a beaucoup de champs.

  • À la déclaration de la variable :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable = { 'a', 12345, 3.141592 };

    Les valeurs des champs sont assignés dans l'ordre où ils sont déclarés. S'il manque des initialisations, les champs seront initialisés à 0. L'inconvénient, c'est qu'on doit connaitre l'ordre où sont déclarés les champs, ce qui peut être tout aussi pénible à retrouver, et peut causer des plantages lorsque la définition de la structure est modifiée.

  • Une nouveauté du C99 permet d'initialiser certains champs à la déclaration de la variable, en les nommant :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable = { .pi = 3.141592, .ch = 'a', .nb = 12345 };

    Les champs non initialisés seront mis à leur valeur par défaut.

Manipulation

La seule opération prise en charge par le langage est la copie, lors des affectations ou des passages de paramètres à des fonctions. Toutes les autres opérations sont à la charge du programmeur, notamment la comparaison d'égalité (cf. section suivante).

Alignement et bourrage (padding)

Il s'agit d'un concept relativement avancé, mais qu'il est bien de connaitre pour agir en connaissance de cause. Lorsqu'on déclare une structure, on pourrait naïvement croire que les champs se suivent les uns à la suite des autres en mémoire. Or, il arrive souvent que des octets soient intercalés entre les champs.

Considérez la structure suivante:

struct ma_structure {
    char  champ1;      /*  8 bits */
    int   champ2;      /* 32 bits */
    char  champ3;      /*  8 bits */
};

On pourrait penser que cette structure occupera 6 octets en mémoire, et pourtant, sur une bonne partie des compilateurs, on obtiendrait une taille plus proche des 12 octets.

En fait, les compilateurs insèrent quasiment toujours des octets entre les champs pour pouvoir les aligner sur des adresses qui correspondent à des mots machines. Cela est dû à une limitation de la plupart des processeurs, qui ne peuvent lire des « mots » de plus d'un octet que s'ils sont alignés sur un certain adressage (alors qu'il n'y a pas de contrainte particulière pour lire un seul octet, si le type char est défini sur 8bit).

En se représentant la mémoire comme un tableau continu, on peut tracer le dessin suivant:

   | bloc N        | bloc N + 1    | bloc N + 2    |
---+---------------+---------------+---------------+---
   | a | b | c | d | e | f | g | h | i | j | k | l |
---+---------------+---------------+---------------+---

Les cases a, b, c, ... représentent des octets, et les blocs des sections de 32 bits. Si on suppose qu'une variable de type ma_structure doive être placée en mémoire à partir du bloc numéro N, alors un compilateur pourra, pour des raisons de performance, placer champ1 en a, champ2 de e à h, et champ3 en i. Cela permettrait en effet d'accéder simplement à champ2: le processeur fournit des instructions permettant de lire ou d'écrire directement le bloc N + 1. Dans ce cas, les octets de b à d ne sont pas utilisés; on dit alors que ce sont des octets de bourrage (ou padding en anglais). Un autre compilateur (ou le même, appelé avec des options différentes) peut aussi placer champ2 de b à e, et champ3 en f, pour optimiser l'utilisation mémoire. Mais alors il devra générer un code plus complexe lors des accès à champ2, le matériel ne lui permettant pas d'accéder en une seule instruction aux 4 octets b à e.

En fait il faut garder à l'esprit que toutes les variables suivent cette contrainte: aussi bien les variables locales aux fonctions, les champs de structures, les paramètres de fonctions, etc.

L'existence d'octets de bourrage ainsi que leur nombre sont non seulement dépendants de l'architecture, mais aussi du compilateur. Cela dit, il est toujours possible de connaître la « distance » (offset) d'un champ par rapport au début de la structure, et ce, de manière portable. Pour cela il existe une macro, déclarée dans l'entête <stddef.h>:

size_t offsetof(type, champ);

La valeur renvoyée est le nombre de char (i.e. d'octets la plupart du temps), entre le début de la structure et celui du champ. Le premier argument attendu est bel et bien le type de la structure et non une variable de ce type, ni un pointeur vers une variable de ce type. Pour s'en souvenir, il suffit de savoir que beaucoup d'implémentations d'offsetof utilisent une arithmétique de ce genre:

size_t distance = (size_t) &((type *)NULL)->champ;

Si type était un pointeur, il faudrait faire un déréférencement supplémentaire (ou éviter l'étoile dans la macro). À noter que, même si cette macro peut s'avérer contraignante (notamment lorsqu'on ne dispose que de type pointeur), il est quand même préférable de l'utiliser pour des raisons de portabilité.

Voici un petit exemple d'utilisation de cette macro:

#include <stddef.h>
#include <stdio.h>

struct ma_structure {
    char  champ1;
    int   champ2;
    char  champ3;
};

int main(void)
{
    /* en C99 */
    /*
    printf("L'offset de 'champ2' vaut %zu.\n",
            offsetof(struct ma_structure, champ2));
    */

    /* en C90 */
    printf("L'offset de 'champ2' vaut %lu.\n",
            (unsigned long) offsetof(struct ma_structure, champ2));
    return 0;
}

Sur une architecture 32 bits, vous obtiendrez très certainement la réponse:

L'offset de 'champ2' vaut 4.

En fait toute cette section était pour souligner le fait qu'il est difficilement portable de comparer les structures comme des blocs binaires (via la fonction memcmp par exemple), car il n'y a aucune garantie que ces octets de bourrage soient initialisés à une certaine valeur. De la même manière, il est sage de prendre quelques précautions avant de transférer cette structure à l'extérieur du programme (comme un fichier, un tube de communication ou une socket IP). En général, il préférable de traiter la structure champ par champ, pour ce genre d'opérations.

Mais comme rien n'est toujours totalement négatif, les conséquences du bourrage offre énormément de souplesse : imaginons 2 structures:

struct str {
    char *string;
};
struct str_ok {
    char *string;
    size_t len;
};

quelle que soit la structure utilisé, on peut avec l'adresse et la cast (struct str *) accéder à la chaine.

int main (void)
{
    struct str s = { "chaine" };
    struct str_ok sok = { "chaine", 0 };
    void *p = &sok;
    if (((struct str *)p)->string)
    {
        sok.len = strlen(sok.string);
        printf("%s\n", ((struct str *)p)->string);
    }
    /* en revanche: Peut-être très dangereux ! */
    p = &s;
    printf("%s\n",((struct str_ok *)p)->string);
    return 0;
}
  1. ↑ Le lecteur intéressé pourra récupérer la dernière version de travail sur la page "projects" du site du WG14.
  2. ↑ Certaines normes sont rendues disponibles gratuitement et en libre téléchargement sur internet, mais leur nombre est relativement faible par rapport à l'ensemble des normes existantes.
  3. ↑ On peut citer comme exemple les commentaires de fin de ligne, les macros à nombre variable d'arguments, les déclarations de symboles à n'importe quel endroit et le mot clé inline pour pallier la précarité des macros
  4. ↑ L'exemple le plus simple serait la définition d'une variable ayant pour identifieur un mot réservé du C++, comme int class;.
  5. ↑ Le lecteur curieux pourra se référer à cet article de David R. Tribble, en anglais, qui énumère les différences entre C99 et C++98.
  6. ↑ La norme C contraint les domaines de valeurs des types signés entre -(2N-1-1) et 2N-1-1, où N est un entier quelconque, et les types non signés entre 0 et 2N-1.
  7. ↑ La liste des types successifs utilisés pour déterminer le type d'une constante entière a changé entre C90 et C99. Cela n'a, quasiment tout le temps, aucune incidence, sauf pour le cas d'une constante de valeur trop grande pour le type long : en C90, le type unsigned long est utilisé, alors qu'en C99 le type long long sera utilisé. Une telle valeur sera alors signée dans un cas, et non signée dans l'autre. Dans ce cas, utiliser un suffixe adapté (UL, LL ou ULL) permet de s'assurer du type de la constante.
  8. ↑ 0,110 = 0.0001100110011...2
  9. ↑ La confusion est surtout née de l'utilisation du mot byte pour désigner la taille du type char dans la norme, alors que ce mot est utilisé dans presque tous les autres domaines de l'informatique pour désigner un octet.
  10. ↑ En occident, en-dehors des codages compatibles avec ASCII, on ne rencontre guère en pratique que la famille de jeux de caractères EBCDIC, utilisée sur des systèmes IBM et quelques mainframes.
  11. ↑ Le site Unicode publie une liste de bibliothèque de gestion des caractères et chaînes Unicodes. http://www.unicode.org/resources/libraries.html
  12. ↑ Ces trois expressions sont définies sous formes de macro pour le préprocesseur.
  13. ↑ La norme C99 a ajouté le type _Bool, mais celui-ci est peu utilisé, l'usage des types entiers pour représenter les booléens en C étant très répandu (voir Booléens).
  14. ↑ http://www.lipn.fr/~cerin/SE/SeETlangC.pdf
  15. ↑ http://www.siteduzero.com/tutoriel-3-14052-lire-et-ecrire-dans-des-fichiers.html#ss_part_4
  16. ↑ http://c.developpez.com/faq/?page=fichiers#FICHIERS_copier

Pointeurs vers structures

Il est (bien entendu) possible de déclarer des variables de type pointeur vers structure :

struct ma_struct * ma_variable;

Comme pour tout pointeur, on doit allouer de la mémoire pour la variable avant de l'utiliser :

ma_variable = malloc( sizeof(struct ma_struct) );

L'accès aux champs peut se faire comme pour une variable de type structure « normale » :

(* ma_variable).champ

Ce cas de figure est en fait tellement fréquent qu'il existe un raccourci pour l'accès aux champs d'un pointeur vers structure :

ma_variable->champ

Unions

Une union et un enregistrement se déclarent de manière identique :

union type_union
{
    type1 champ1;
    type2 champ2;
    /* ... */
    typeN champN;
};

/* Déclaration de variables */
union type_union var1, var2, /* ... */ varM;

Toutefois, à la différence d'un enregistrement, les N champs d'une instance de cette union occupent le même emplacement en mémoire. Modifier l'un des champ modifie donc tous les champs de l'union. Typiquement, une union s'utilise lorsqu'un enregistrement peut occuper plusieurs fonctions bien distinctes et que chaque fonction ne requiert pas l'utilisation de tous les champs.

Voici un exemple ultra classique d'utilisation des unions, qui provient en fait de la gestion des événements du système X-Window, très répandu sur les systèmes Unix :

union _XEvent
{
    int type;

    XAnyEvent xany;
    XKeyEvent xkey;
    XButtonEvent xbutton;
    XMotionEvent xmotion;
    XCrossingEvent xcrossing;
    XFocusChangeEvent xfocus;
    XExposeEvent xexpose;
    /* ... */
    XErrorEvent xerror;
    XKeymapEvent xkeymap;
    long pad[24];
};

La déclaration a juste été un peu raccourcie pour rester lisible. Les types X*Event de l'union sont en fait des structures contenant des champs spécifiques au message. Par exemple, si type vaut ButtonPress, seules les valeurs du champ xbutton seront significatives. Parmi ces champs, il y a button qui permet de savoir quel bouton de la souris à été préssé :

    XEvent ev;
    XNextEvent(display, &ev);

    switch (ev.type) {
    case ButtonPress:
        printf("Le bouton %d a été pressé.\n", ev.xbutton.button);
        break;
    default:
        printf("Message type %d\n", ev.type);
    }

En offrant une souplesse nécessaire à la manipulation de l'objet, la gestion de la mémoire en C peut aider à réduire les erreurs de programmation lié à la structure de la mémoire (erreur de segmentation/segfault/"Argh!!!"):

#include <stdio.h>
#include <string.h>

struct s1 {
    char *string;
} s1;

struct s2 {
    char   *string;
    size_t len;
    int    ______;
    int    setnull;
} s2;

union str {
    struct s2 s_;
    struct s1 _s;
}str;

typedef void * STR_s1;
typedef void * STR_s2;

STR_s2
string_analyse(STR_s1 *s)
{
    static union str us, init_us = {{NULL,0,0,0}};
    memcpy((void *)&us, (void *)&init_us, sizeof(union str));
    if (!((struct s1 *)s)->string)
    {
        return &us._s;
    }
    us.s_.len = strlen(((struct s1 *)s)->string);
    if (!us.s_.len)
    {
        us.s_.setnull = 1;
        return &us._s;
    }
    us.s_.string = ((struct s1 *)s)->string;
    return &us.s_;
}

int main(void)
{
    struct s1 s = {NULL}, s_ = {"salut"}, s__ = {""};
    void *p;
    p = string_analyse((void *)&s);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    p = string_analyse((void *)&s_);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    p = string_analyse((void *)&s__);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    printf("Réalisé en toute sécurité...\n"
        "Même pour un petit vaisseau serial killer du monde d'en haut.\n");
    return 0;
}

Tout en permettant l'optimisation:

struct s1{
    size_t len;
    char   *str;
} s1;

union s {
    struct s1  dbl[2];
    void       *perform[5];
    struct s1  simple;
};

void * | void * | void * | void * | void *
size_t | char * | size_t | char * | ---
size_t | char * | ------ |  | ---
n oct | n oct | n oct | n oct | 2int=n oct

ce qui signifie que:

int main(void)
{
    const char *end[2] = {", ", "\n"};
    union s init = {{{5,"Salut"}, {3,"toi"}}}, us;
    void *p;
    memset((void *)&us, 0,40);
    memcpy((void *)&us,
            (void *)&init,
            2 * sizeof(struct s1));
    for (p = &us;
        ((union s *)p)->perform[0] != (void *)0;
        p += 16)
        printf("%s%s",
                (char *)((union s *)p)->perform[1],
                end[
                     !((char *)((union s *)p)->perform[2])]
                );
    return 0;
}

Définitions de synonymes de types (typedef)

Le langage C offre un mécanisme assez pratique pour définir des synonymes de types. Il s'agit du mot-clé typedef.

typedef un_type synonyme_du_type;

Contrairement aux langages à typage fort comme le C++, le C se base sur les types atomiques pour décider de la compatibilité entre deux types. Dit plus simplement, la définition de nouveaux types est plus un mécanisme d'alias qu'une réelle définition de type. Les deux types sont effectivement parfaitement interchangeables. À la limite on pourrait presque avoir les mêmes fonctionnalités en utilisant le préprocesseur C, bien qu'avec ce dernier vous aurez certainement beaucoup de mal à sortir de tous les pièges qui vous seront tendus.

Quelques exemples

typedef unsigned char octet;
typedef double matrice4_4[4][4];
typedef struct ma_structure * ma_struct;
typedef void (*gestionnaire_t)( int );

/* Utilisation */
octet nombre = 255;
matrice4_4 identite = { {1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0,0,1} };
ma_struct pointeur = NULL;
gestionnaire_t fonction = NULL;

Ce mot clé est souvent utilisé conjointement avec la déclaration des structures, pour ne pas devoir écrire à chaque fois le mot clé struct. Elle permet aussi de grandement simplifier les prototypes de fonctions qui prennent des pointeurs sur des fonctions en argument, ou retournent de tels types. Il est conseillé dans de tels cas de définir un type synonyme, plutôt que de l'écrire in extenso dans la déclaration du prototype. Considérez les deux déclarations :

/* Déclaration confuse */
void (*fonction(int, void (*)(int)))(int);

/* Déclaration claire avec typedef */
typedef void (*handler_t)(int);

handler_t fonction(int, handler_t);

Les vétérans auront reconnu le prototype imbitable de l'appel système signal(), qui permet de rediriger les signaux (interruption, alarme périodique, erreur de segmentation, division par zéro, ...).

Énumérations

enum nom_enum { val1, val2, ..., valN };

Les symboles val1, val2, ..., valN pourront être utilisés littéralement dans la suite du programme. Ces symboles sont en fait remplacés par des entiers lors de la compilation. La numérotation commençant par défaut à 0, s'incrémentant à chaque déclaration. Dans l'exemple ci-dessus, val1 vaudrait 0, val2 1 et valN N-1.

On peut changer à tout moment la valeur d'un symbole, en affectant au symbole, la valeur constante voulue (la numérotation recommençant à ce nouvel indice). Par exemple :

enum Booleen { Vrai = 1, Faux = 0 };

/* Pour l'utiliser */
enum Booleen variable = Faux;

Ce qui est assez pénible en fait, puisqu'il faut à chaque fois se souvenir que le type Booleen est dérivé d'une énumération. Il est préférable de simplifier les déclarations, grâce à l'instruction typedef :

typedef enum { Faux, Vrai } Booleen;

/* Pour l'utiliser */
Booleen variable = Faux;

Type incomplet

Pour garantir un certain degré d'encapsulation, il peut être intéressant de masquer le contenu d'un type complexe, pour éviter les usages trop « optimisés » de ce type. Pour cela, le langage C permet de déclarer un type sans indiquer explicitement son contenu.

struct ma_structure;

/* Plus loin dans le code */
struct ma_structure * nouvelle = alloue_objet();

Les différents champs de la structure n'étant pas connus, le compilateur ne saura donc pas combien de mémoire allouer. On ne peut donc utiliser les types incomplets qu'en tant que pointeur. C'est pourquoi, il est pratique d'utiliser l'instruction typedef pour alléger les écritures :

typedef struct ma_structure * ma_struct;

/* Plus loin dans le code */
ma_struct nouvelle = alloue_objet();

Cette construction est relativement simple à comprendre, et proche d'une conception objet. À noter que le nouveau type défini par l'instruction typedef peut très bien avoir le même nom que la structure. C'est juste pour éviter les ambiguités, qu'un nom différent a été choisi dans l'exemple.

Un autre cas de figure relativement classique, où les types incomplets sont très pratiques, ce sont les structures s'auto-référençant, comme les listes chainées, les arbres, etc.

struct liste
{
    struct liste * suivant;
    struct liste * precedant;
    void *         element;
};

Ou de manière encore plus tordue, plusieurs types ayant des références croisées :

struct type_a;
struct type_b;

struct type_a
{
    struct type_a * champ1;
    struct type_b * champ2;
    int             champ3;
};

struct type_b
{
    struct type_a * ref;
    void *          element;
};

Préprocesseur

Le préprocesseur est un langage de macro qui est analysé, comme son nom l'indique, avant la compilation. En fait, c'est un langage complètement indépendant, il est même théoriquement possible de l'utiliser par dessus un autre langage que le C. Cette indépendance fait que le préprocesseur ignore totalement la structure de votre programme, les directives seront toujours évaluées de haut en bas.

Ces directives commencent toutes par le symbole dièse (#), suivi d'un nombre quelconque de blancs (espace ou tabulation), suivi du nom de la directive en minuscule. Les directives doivent être déclarées sur une ligne dédiée. Les noms standards de directives sont :

  • define : définit un motif de substitution.
  • undef : retire un motif de substitution.
  • include : inclusion de fichier.
  • ifdef, ifndef, if, else, elif, endif : compilation conditionnelle.
  • pragma : extension du compilateur.
  • error : émettre un message d'erreur personnalisé et stopper la compilation.

Variables de substitution et macros

Déclarations de constantes

Les variables de substitution du préprocesseur fournissent un moyen simple de nommer des constantes. En effet :

#define CONSTANTE valeur

permet de substituer presque partout dans le code source qui suit cette ligne la suite de caractères « CONSTANTE » par la valeur. Plus précisément, la substitution se fait partout, à l'exception des caractères et des chaines de caractères. Par exemple, dans le code suivant :

#define TAILLE 100

printf("La constante TAILLE vaut %d\n", TAILLE);

La substitution se fera sur la deuxième occurrence de TAILLE, mais pas la première. Le préprocesseur transformera ainsi l'appel à printf :

printf("La constante TAILLE vaut %d\n", 100);

Le préprocesseur procède à des traitements sur le code source, sans avoir de connaissance sur la structure de votre programme. Dans le cas des variables de substitution, il ne sait faire qu'un remplacement de texte, comme le ferait un traitement de texte. On peut ainsi les utiliser pour n'importe quoi, des constantes, des expressions, voire du code plus complexe ; le préprocesseur n'ira pas vérifier si la valeur de remplacement est une expression C ou non. Dans l'exemple précédent, nous avons utilisé TAILLE pour remplacer la constante 100, mais toute suite de caractères peut être définie. Par exemples:

#define PREFIXE "erreur:"
#define FORMULE (1 + 1)

printf(PREFIXE "%d\n", FORMULE);

Ici, l'appel à printf sera transformé ainsi :

printf("erreur:" "%d\n", (1 + 1));

Une définition de constantes peut s'étendre sur plusieurs lignes. Pour cela, il faut que le dernier caractère de la ligne soit une barre oblique inverse ('\'). On peut ainsi définir une macro qui est remplacée par un bout de code plus complexe :

#define BLOC_DEFAUT            \
    default:                   \
        puts("Cas interdit."); \
        break;

Cette variable de substitution permet ainsi d'ajouter le cas par défaut à un switch.

Historiquement, les programmeurs avaient pour habitude d'utiliser des capitales pour distinguer les déclarations du préprocesseur et les minuscules pour les noms de symboles (fonctions, variables, champs, etc.) du compilateur. Ce n'est pas une règle à suivre impérativement, mais elle améliore la lisibilité des programmes.

Il est possible de définir plusieurs fois la même « CONSTANTE ». Le compilateur n'émettra un avertissement que si les deux valeurs ne concordent pas.

En fait, dans la section précédente, il était fait mention d'un mécanisme relativement similaire : les énumérations. On peut légitimement se demander ce que ces énumérations apportent en plus par rapport aux directives du préprocesseur. En fait, on peut essentiellement souligner que :

  • Lors des cas multiples (switch), le compilateur peut vérifier que l'ensemble des cas couvre l'intervalle de valeur du type, et émettre un avertissement si ce n'est pas le cas. Ce qui est évidemment impossible à faire avec des #define.
  • Là où l'utilité est plus flagrante, c'est lors du débogage. Un bon débogueur peut afficher le nom de l'élément énuméré, au lieu de simplement une valeur numérique, ce qui rends un peu moins pénible ce processus déjà très rébarbatif à la base, surtout lorsque le type en question est une structure avec des dizaines, pour ne pas dire une centaine, de champs.
  • Certains compilateurs (gcc pour ne citer que le plus connu) n'incluent pas par défaut les symboles du préprocesseur avec l'option standard de débogage (-g), principalement pour éviter de faire exploser la taille de l'exécutable. Si bien que dans un débogueur il est souvent impossible d'afficher la valeur associée à une constante du préprocesseur autrement qu'en fouillant dans les sources. À moins d'avoir un environnement plutôt évolué, cette limitation peut s'avérer très pénible.

Déclarations automatiques

Le langage C impose que le compilateur définisse un certain nombre de constantes. Sans énumérer toutes celles spécifiques à chaque compilateur, on peut néanmoins compter sur :

  • __FILE__ (char *) : une chaîne de caractères représentant le nom de fichier dans lequel on se trouve. Pratique pour diagnostiquer les erreurs.
  • __LINE__ (int) : le numéro de la ligne en cours dans le fichier.
  • __DATE__ (char *) : la date en cours (incluant le jour, le mois et l'année).
  • __TIME__ (char *) : l'heure courante (HH:MM:SS).
  • __STDC__ (int) : cette constante est en général définie si le compilateur suit les règles du C ANSI (sans les spécificités du compilateur). Cela permet d'encadrer des portions de code non portables et fournir une implémentation moins optimisée, mais ayant plus de chance de compiler sur d'autres systèmes.

Extensions

Chaque compilateur peut définir d'autres macro, pourvu qu'elles restent dans les conventions de nommage que la norme leur réserve. Les lister toutes ici est impossible et hors-sujet, mais on peut citer quelques unes, à titre d'exemple, que le lecteur pourra rencontrer régulièrement dans du code. Il va de soi que le fait qu'elles soient définies ou non, et leur éventuelle valeur, est entièrement dépendant du compilateur.

  • Détection du système d'exploitation :
    • _WIN32 ou __WIN32__ (Windows)
    • linux ou __linux__ (Linux)
    • __APPLE__ ou __MACH__ (Apple Darwin)
    • __FreeBSD__ (FreeBSD)
    • __NetBSD__ (NetBSD)
    • sun ou __SVR4 (Solaris)
  • Visual C++ : _MSC_VER
  • Compilateur gcc :
    • Version : __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__.
  • Compilateurs Borland :
    • __TURBOC__ (version de Turbo C ou Borland C)
    • __BORLANDC__ (version de Borland C++ Builder)
  • Divers : __MINGW32__ (MinGW), __CYGWIN__ ou __CYGWIN32__ (Cygwin),

Déclaration de macros

Une macro est en fait une constante qui peut prendre un certain nombre d'arguments. Les arguments sont placés entre parenthèses après le nom de la macro sans espaces, par exemple :

#define MAX(x,y) x > y ? x : y
#define SWAP(x,y) x ^= y, y ^= x, x ^= y

La première macro prend deux arguments et « retourne » le maximum entre les deux. La deuxième est plus subtile, elle échange la valeur des deux arguments (qui doivent être des variables entières), sans passer par une variable temporaire, et ce avec le même nombre d'opérations.

La macro va en fait remplacer toutes les occurrences de la chaîne « MAX » et de ses arguments par « x > y ? x : y ». Ainsi, si on appelle la macro de cette façon :

printf("%d\n", MAX(4,6));

Elle sera remplacée par :

printf("%d\n", 4 > 6 ? 4 : 6);

Il faut bien comprendre qu'il ne s'agit que d'une substitution de texte, qui ne tient pas compte de la structure du programme. Considérez l'exemple suivant, qui illustre une erreur très classique dans l'utilisation des macros :

#define MAX(x,y) x > y ? x : y

i = 2 * MAX(4,6); /* Sera remplacé par : i = 2 * 4 > 6 ? 4 : 6; */

L'effet n'est pas du tout ce à quoi on s'attendait. Il est donc important de bien parenthéser les expressions, justement pour éviter ce genre d'effet de bord. Il aurait mieux fallu écrire la macro MAX de la manière suivante :

#define MAX(x,y) ((x) > (y) ? (x) : (y))

En fait dès qu'une macro est composée d'autre chose qu'un élément atomique (un lexème, ou un token) il est bon de le mettre entre parenthèses, notamment les arguments qui peuvent être des expressions utilisant des opérateurs ayant des priorités arbitraires.

Il est a noter que l'emploi systématique de parenthèses ne protège pas contre tous les effets de bord. En effet :

MAX(x++,y); /* Sera remplacé par : ((x++) > (y) ? (x++) : (y)) */

Du coup, x est incrémenté de 2 et non pas de 1. La qualité et la performance des compilateurs C modernes fait que l'utilisation de fonctions inline est le plus souvent préférable.

Suppression d'une définition

Il arrive qu'une macro/constante soit déjà définie, mais qu'on aimerait quand même utiliser ce nom avec une autre valeur. Pour éviter un avertissement du préprocesseur, on doit d'abord supprimer l'ancienne définition, puis déclarer la nouvelle :

#undef symbole
#define symbole nouvelle_valeur

Cette directive supprime le symbole spécifié. À noter que même pour les macros avec arguments, il suffit juste de spécifier le nom de cette macro. Qui plus est, supprimer une variable de substitution inexistante ne provoquera aucune erreur.

Transformation en chaîne de caractères

Le préprocesseur permet de transformer une expression en chaîne de caractères. Cette technique ne fonctionne donc qu'avec des macros ayant au moins un argument. Pour transformer n'importe quel argument de la macro en chaîne de caractères, il suffit de préfixer le nom de l'argument par le caractère dièse ('#'). Cela peut être utile pour afficher des messages de diagnostique très précis, comme dans l'exemple suivant :

#define assert(condition) \
if( (condition) == 0 ) \
{ \
        puts( "La condition '" #condition "' a échoué" ); \
        exit( 1 );            \
}

À noter qu'il n'existe pas de mécanisme simple pour transformer la valeur de la macro en chaîne de caractères. C'est le cas classique des constantes numériques qu'on aimerait souvent transformer en chaîne de caractères : le préprocesseur C n'offre malheureusement rien de vraiment pratique pour effectuer ce genre d'opération.

Concaténation d'arguments

Il s'agit d'une facilité d'écriture pour simplifier les tâches répétitives. En utilisant l'opérateur ##, on peut concaténer deux expressions :

#define version(symbole) symbole ## _v123

int version(variable); /* Déclare "int variable_v123;" */

Déclaration de macros à nombre variable d'arguments

Ceci est une nouveauté du C99. La déclaration d'une macro à nombre variable d'arguments est en fait identique à une fonction, sauf qu'avec une macro on ne pourra pas traiter les arguments supplémentaires un à un. Ces paramètres sont en fait traités comme un tout, via le symbole __VA_ARGS__. Exemple :

#define debug(message, ...) fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, __VA_ARGS__ )

Il y a une restriction qui ne saute pas vraiment aux yeux dans cet exemple, c'est que les points de suspension doivent obligatoirement être remplacés par au moins un argument, ce qui n'est pas toujours très pratique. Malheureusement le langage C n'offre aucun moyen pour contourner ce problème pourtant assez répandu.

À noter, une extension du compilateur gcc, qui permet de s'affranchir de cette limitation en rajoutant l'opérateur ## à __VA_ARGS__ :

/* Extension de gcc pour utiliser la macro sans argument */
#define debug(message, ...) fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, ##__VA_ARGS__ )

Ces macros peuvent être utilisées par exemple pour des traitements autour de la fonction printf, voir par exemple Variadic Macros anglais.

Exemples

Les macro du préprocesseur sont souvent utilisées dans des situations où une fonction, éventuellement inline, aurait le même effet, avec une meilleure robustesse. Cependant, il y a des usages pour lesquels les macros ne peuvent être remplacés par des fonctions.

Par exemple, pour afficher la taille des types C, on écrirait un programme comme le suivant :

#include <stdio.h>

int main(void)
{
    printf("sizeof(char) = %zd.\n", sizeof(char));
    printf("sizeof(short) = %zd.\n", sizeof(short));
    printf("sizeof(int) = %zd.\n", sizeof(int));
    /* ...*/
    return 0;
}

Écrire un tel programme peut être fatiguant, et il est très facile de faire des erreurs en recopiant les lignes. Ici, une fonction ne pourrait simplifier l'écriture. En effet, il faudrait passer à la fonction le nom du type, pour l'afficher dans la chaîne "sizeof(XXX) = ", et la taille. On devrait donc donner deux arguments à la fonction, pour l'appeler ainsi:

print_taille("char", sizeof(char));

Ce qui ne serait pas beaucoup plus simple, surtout que la fonction elle-même serait plus complexe (utilisation de fonctions str* pour inclure le premier paramètre dans la chaîne de format). Ici, le préprocesseur fournit une solution beaucoup plus simple, à l'aide de l'opérateur # :

#include <stdio.h>

#define PRINTF_SIZEOF(type) printf("sizeof(" #type ") = %zd.\n", sizeof(type))

int main(void)
{
    PRINTF_SIZEOF(char);
    PRINTF_SIZEOF(short);
    PRINTF_SIZEOF(int);
    return 0;
}

Une technique similaire peut être utilisée pour conserver à l'affichage le nom de constantes ou d'énumérations :

#include <stdio.h>
enum etat_t { Arret, Demarrage, Marche, ArretEnCours };

#define CASE_ETAT(etat) case etat: printf("Dans l'état " #etat "\n"); break;

void affiche_etat(etat_t etat)
{
  switch (etat)
  {
     CASE_ETAT(Arret)
     CASE_ETAT(Demarrage)
     CASE_ETAT(Marche)
     CASE_ETAT(ArretEnCours)
     default:
         printf("Etat inconnu (%d).\n", etat);
         break;
  }
}

Ces exemples sont tirés d'un message de Bill Godfrey sur comp.lang.c.

Compilation conditionnelle

Les tests permettent d'effectuer de la compilation conditionnelle. La directive #ifdef permet de savoir si une constante ou une macro est définie. Chaque déclaration #ifdef doit obligatoirement se terminer par un #endif, avec éventuellement une clause #else entre. Un petit exemple classique :

#ifdef DEBUG
/* S'utilise : debug( ("Variable x = %d\n", x) ); (double parenthésage) */
#define debug(x) printf x
#else
#define debug(x)
#endif

L'argument de la directive #ifdef doit obligatoirement être un symbole du préprocesseur. Pour utiliser des expressions un peu plus complexes, il y a la directive #if (et #elif, contraction de else if). Cette directive utilise des expressions semblable à l'instruction if() : si l'expression évaluée est différente de zéro, elle sera considéré comme vraie, et comme fausse si l'expression s'évalue à 0.

On peut utiliser l'addition, la soustraction, la multiplication, la division, les opérateurs binaires (&, |, ^, ~, <<, >>), les comparaisons et les connecteurs logiques (&& et ||). Ces derniers sont évalués en circuit court comme leur équivalent C. Les opérandes possibles sont les nombres entiers et les caractères, ainsi que les macros elle-mêmes, qui seront remplacés par leur valeur.

À noter, l'opérateur spécial defined qui permet de tester si une macro est définie (et qui renvoit donc un booléen). Exemple :

#if defined(DEBUG) && defined(NDEBUG)
#error DEBUG et NDEBUG sont définis !
#endif

Cette directive est très pratique pour désactiver des pans entier de code sans rien y modifier. Il suffit de mettre une expression qui vaudra toujours zéro, comme :

#if 0
/* Vous pouvez mettre ce que vous voulez ici, tout sera ignoré, même du code invalide */
:-)
#endif

Un autre avantage de cette technique, est que les directives de compilations peuvent être imbriquées, contrairement aux commentaires. Dans ce cas, s'il y avait eu d'autres directives #if / #endif (correctement balancées), la clause #if 0 ne s'arrêterait pas au premier #endif rencontré, tandis que cela aurait été le cas avec des commentaires.

À noter qu'au niveau du préprocesseur il est impossible d'utiliser les opérateurs du C. Notamment l'opérateur sizeof, dont le manque aura fait grincer les dents à des générations de programmeurs, sera en fait interprété comme étant une macro. Il faut bien garder à l'esprit que le préprocesseur C est totalement indépendant du langage C.

Inclusion de fichiers

Il s'agit d'une autre fonctionnalité massivement employée dans toute application C qui se respecte. Comme son nom l'indique, la directive #include permet d'inclure in extenso le contenu d'un autre fichier, comme s'il avait été écrit en lieu et place de la directive. On peut donc voir ça comme de la factorisation de code, ou plutôt de déclarations. On appelle de tels fichiers, des fichiers en-têtes (header files) et on leur donne en général l'extension .h.

Il est rare d'inclure du code dans ces fichiers (définitions de variables ou de fonctions), principalement parce que ces fichiers sont destinés à être inclus dans plusieurs endroits du programme. Ces définitions seraient donc définies plusieurs fois, ce qui, dans le meilleur des cas, seraient du code superflu, et dans le pire, pourraient poser des problèmes à l'édition des liens.

On y place surtout des déclarations de type, macros, prototypes de fonctions relatif à un module. On en profite aussi pour documenter toutes les fonctions publiques, leurs paramètres, les valeurs renvoyées, les effets de bords, la signification des champs de structures et les pré/post conditions (quel état doit respecter la fonction avant/après son appel). Dans l'idéal on devrait pouvoir comprendre le fonctionnement d'un module, simplement en lisant son fichier en-tête.

La directive #include prend en fait un argument : le nom du fichier que vous voulez inclure. Ce nom doit être soit mis entre guillemets doubles ou entre balises (< >). Cette différence affecte simplement l'ordre de recherche du fichier. Dans le premier cas, le fichier est recherché dans le répertoire où le fichier contenant la directive se trouve, puis le préprocesseur regarde à des endroits préconfigurés. Dans le second cas, il regardera seulement dans les endroits préconfigurés. Les endroits préconfigurés sont le répertoire include par défaut (par exemple /usr/include/, sous Unix) et ceux passés explicitement en paramètre au compilateur.

En fait l'argument de la directive #include peut aussi être une macro, dont la valeur est un argument valide (soit une chaîne de caractères, soit un nom entre balises < et >) :

#define FICHIER_A_INCLURE <stdio.h>
#include FICHIER_A_INCLURE

Exemple

  • fichier.h
void affiche( void );
  • fichier.c
#include "fichier.h"

int main( void )
{
        affiche();
        return 0;
}

C'est comme si fichier.c avait été écrit :

void affiche( void );

int main( void )
{
        affiche();
        return 0;
}

Protection contre les inclusions multiples

À noter un problème relativement récurrent avec les fichiers en-têtes : il s'agit des inclusions multiples. À mesure qu'une application grandit, il arrive fréquemment qu'un fichier soit inclus plusieurs fois à la suite d'une directive #include. Quand bien même les déclarations sont identiques, définir deux types avec le même nom n'est pas permis en C. On peut néanmoins s'en sortir avec cette technique issue de la nuit des temps :

#ifndef H_MON_FICHIER
#define H_MON_FICHIER

/* Mettre toutes les déclarations ici */

#endif

C'est un problème tellement classique, que le premier réflexe lors de l'écriture d'un tel fichier est de rajouter ces directives. H_MON_FICHIER est bien-sûr à adapter à chaque fichier, l'essentiel est que le nom soit unique dans toute l'application. Habituellement on utilise le nom du fichier, mis en majuscule, avec les caractères non-alphabétiques remplacés par des soulignés.

Avertissement et message d'erreur personnalisés

Il peut être parfois utile d'avertir l'utilisateur que certaines combinaisons d'options sont dangereuses ou carrément invalides. Le préprocesseur dispose d'une directive pour effectuer cela:

#error "message d'erreur"

Cette directive affichera en fait le message tel quel, qui peut évidemment contenir n'importe quoi, y compris des caractères réservés, sans qu'on ait besoin de le mettre entre guillemets ("). Après émission du message, la compilation s'arrêtera.

À noter que certains compilateurs comme GCC proposent aussi la directive:

#warning "message d'avertissement"

qui permet d'afficher un message sans arrêter la compilation.

Bibliothèque standard

La bibliothèque standard du langage C peut paraître relativement pauvre par rapport à d'autres langages tout en un plus récents comme Python, Ruby, Perl, C# ou Java. Conçue avant tout avec un souci de portabilité, et en ayant en tête les contraintes matérielles limitées de certaines plate-formes auxquelles le C est aussi destiné, vous obtenez avec cela le plus petit dénominateur commun qui puisse être porté sur le plus grand nombre de plateformes. Les contraintes qu'ont subies l'ANSI, et le WG14 après lui, portant principalement sur les contraintes de portabilité sur certaines architectures parfois « exotiques », et la très forte contrainte de ne pas « casser » du code existant reposant sur un comportement déjà établi, voire normalisé, font que certains points connus pour être complexes, voire peu souhaitables, sont restés dans le langage et la bibliothèque standard.

Concevoir une application avec les seules fonctions présentes dans cette bibliothèque nécessite une très grande rigueur. Le WG14 ne s'étant pas fixé pour but d'étendre la bibliothèque standard de manière importante, il est plus que conseillé de se tourner vers des bibliothèques de plus haut niveau, afin d'éviter de réinventer inutilement la roue. Il en existe heureusement beaucoup, mais décrire ne serait-ce que ce qui existe est hors de la portée de cet ouvrage.

La bibliothèque standard permet toutefois de faire des traitements usuels avec peu d'efforts, pour peu qu'on ait conscience des dangers et des pièges qui sont parfois tendus. Les sections qui suivent permettront de voir un peu plus clair dans les méandres parfois très sombres où s'aventure le C.

Voir aussi

Cours sur Wikiversity sur l'utilisation des fonctions standards du langage C.

Chaînes de caractères

Le langage C offre quelques facilités d'écritures pour simuler les chaînes de caractères à l'aide de tableaux. En plus de cela, certaines fonctions de la bibliothèque standard (et les autres) permettent de faciliter leur gestion. À la différence d'un tableau, les chaînes de caractères respectent une convention : se terminer par le caractère nul, noté '\0' (antislash-zero). Ainsi, pour construire une chaîne de caractères « à la main », il ne faut pas oublier ce caractère final.

On peut noter que, par rapport à ce qui est disponible dans d'autres langages, les fonctions de la bibliothèque standard sont peu pratiques à utiliser. On peut avancer sans trop de risque qu'une des plus grandes lacunes du C provient de sa gestion précaire et limitée des chaînes de caractères, pourtant massivement employées dans d'innombrables logiciels. De plus, une mauvaise utilisation de ces fonctions peut facilement conduire à es bugs. Pour faire court, on ne saurait trop conseiller de soit reprogrammer les fonctions ci-dessous, d'utiliser une bibliothèque externe ou de faire preuve de paranoïa avant leur utilisation.

On notera en particulier que la bibliothèque du langage C, assez ancienne n'apporte pas de fonction particulière pour traiter les spécificités de nouveaux standards tels qu'Unicode. Elle est toutefois assez générique pour effectuer certains opérations basiques. Pour une utilisation plus avancée, un bibliothèque Unicode peut avoir un intérêt.

Les fonctions permettant de manipuler les chaînes de caractères se trouvent dans l'entête <string.h>', ainsi pour les utiliser il faut ajouter la commande préprocesseur :

#include <string.h>

Comparaison de chaînes

int strcmp(const char * chaine1, const char * chaine2);
int strncmp(const char * chaine1, const char * chaine2, size_t longueur);

Compare les chaînes chaine1 et chaine2 et renvoie un entier :

  • négatif, si chaine1 est inférieure à chaine2 (avant dans l'ordre alphabétique) ;
  • nul, si chaine1 est égale à chaine2 (.i.e. chaine1 et chaine2 sont identiques) ;
  • positif, si chaine1 est supérieur à chaine2 (après dans l'ordre alphabétique) .

première remarque : lorsque les deux chaînes sont égales, strcmp renvoie 0, qui a la valeur de vérité faux. Pour tester l'égalité entre deux chaînes, il faut donc écrire soit if (strcmp(chaine1, chaine2) == 0) ..., soit if (!strcmp(chaine1, chaine2)) ... mais surtout pas if (strcmp(chaine1, chaine2)) ... qui teste si deux chaînes sont différentes !

seconde remarque : l'opérateur ==, dans le cas de pointeurs, teste si les adresses sont égales. Noter chaine1 == chaine2, si chaine1 et chaine2 sont des char * revient à tester si les deux chaînes pointent sur la même zone mémoire et non pas à tester l'égalité de leur contenu.

troisième remarque : la comparaison effectuée est une comparaison binaire. Deux chaînes canoniquement équivalentes au sens Unicode sont donc ici considérées différentes.

quatrième remarque : la comparaison n'est pas basée sur un alphabet mais sur un codage de caractère. Ceci conduit à un tri particulier des caractères accentuées, majuscules et minuscules,qui n'est pas nécessairement celui souhaité.

À noter l'existence de deux fonctions de comparaisons de chaîne, insensibles à la casse des caractères et s'adaptant à la localisation en cours, fonctionnant sur le même principe que strcmp() et strncmp(), mais dont l'origine provient des systèmes BSD :

int strcasecmp(const char * chaine1, const char * chaine2);
int strncasecmp(const char * chaine1, const char * chaine2, size_t longueur);

Ces fonctions se basent sur les locales [1].

Longueur d'une chaîne

size_t strlen(const char * chaine);

Renvoie la longueur de la chaine en octets, sans compter le marqueur de fin de chaîne '\0'.

exemple : strlen("coincoin") renvoie 8.

Nota: la longueur mesurée en caractères peut être différente de la longueur mesurée en octets suivant le système de codage de caractère utilisé. [2].

Concaténation et copie de chaînes

char * strcpy (char *destination, const char *source);
char * strncpy (char *destination, const char *source, size_t n);
char * strcat (char *destination, const char *source);
char * strncat (char *destination, const char *source, size_t n);

strcpy copie le contenu de source à l'adresse mémoire pointé par destination, incluant le caractère nul final. strcat concatène la chaîne source à la fin de la chaîne destination, en y rajoutant aussi le caractère nul final. Toutes ces fonctions renvoient le pointeur sur la chaîne destination.

Pour strncpy, on notera que la copie est limité à un nombre d'octets:

  • si la chaîne source a moins de n octets/caractères non nuls, strncpy complètera la chaîne destination avec des caractères nuls;
  • si la chaîne source a n octets/caractères non nuls ou plus, la fonction n'insèrera pas de caractère nul à la fin de la chaine destination (i.e. destination ne sera pas une chaîne de caractères valide).

Il faut être très prudent lors des copies ou des concaténations de chaînes, car les problèmes pouvant survenir peuvent être très pénibles à diagnostiquer. L'erreur « classique » est de faire une copie dans une zone mémoire non réservée ou trop petite, comme dans l'exemple suivant, a priori anodin:

Avertissement Ce code contient une erreur volontaire !
char *copie_chaine(const char *source)
{
    char *chaine = malloc(strlen(source));

    if (chaine != NULL)
    {
        strcpy(chaine, source);
    }
    return chaine;
}

Ce code a priori correct provoque pourtant l'écriture d'un caractère dans une zone non allouée. Les conséquences de ce genre d'action sont totalement imprévisibles, pouvant au mieux passer inaperçue, ou au pire écraser des structures de données critiques dans la gestion des blocs mémoires et engendrer des accès mémoire illégaux lors des prochaines tentatives d'allocation ou de libération de bloc, i.e. le cauchemar de tout programmeur. Dans cet exemple il aurait bien évidemment fallu écrire :

       char * chaine = malloc( strlen(source) + 1 );

Une autre erreur, beaucoup plus fréquente hélas, est de copier une chaîne dans un tableau de caractères local, sans se soucier de savoir si ce tableau est capable d'accueillir la nouvelle chaîne. Les conséquences peuvent ici être beaucoup plus graves qu'un simple accès illégal à une zone mémoire globale.

Un écrasement mémoire (buffer overflow) est considéré comme un défaut de sécurité relativement grave, puisque, sur un grand nombre d'architectures, un « attaquant » bien renseigné sur la structure du programme peut effectivement lui faire exécuter du code arbitraire. Ce problème vient de la manière dont les variables locales aux fonctions et certaines données internes sont stockées en mémoire. Comme le C n'interdit pas d'accéder à un tableau en dehors de ses limites, on pourrait donc, suivant la qualité de l'implémentation, accéder aux valeurs stockées au-delà des déclarations de variables locales. En fait, sur un grand nombre d'architectures, les variables locales sont placées dans un espace mémoire appelé pile, avec d'autres informations internes au système, comme l'adresse de retour de la fonction, c'est-à-dire l'adresse de la prochaine instruction à exécuter après la fin de la fonction. En s'y prenant bien, on peut donc écraser cette valeur pour la remplacer par l'adresse d'un autre bout de code, qui donnerait l'ordre d'effacer le disque dur, par exemple ! Si en plus le programme possède des privilèges, les résultats peuvent être assez catastrophiques. Ainsi décrit, le problème semble complexe : il faudrait que l'attaquant puisse insérer dans une zone mémoire de l'application un bout de code qu'il a lui-même écrit, et qu'il arrive à écrire l'adresse de ce bout de code là où le système s'attend à retrouver l'adresse de retour de la fonction. Cependant, ce genre d'attaque est aujourd'hui très courant, et les applications présentant ce genre d'erreur deviennent très rapidement la cible d'attaques.

Voici un exemple très classique, où ce genre d' exploit peut arriver :

Avertissement Ce code contient une erreur volontaire !
int traite_chaine(const char *ma_chaine)
{
    char tmp[512];

    strcpy(tmp, ma_chaine);

    /* ... */
}

Ce code, hélas plus fréquent qu'on ne le pense, est à bannir. Parmi différentes méthodes, on peut éviter ce problème en ne copiant qu'un certain nombre des premiers caractères de la chaîne, avec la fonction strncpy :

char tmp[512];
strncpy(tmp, ma_chaine, sizeof(tmp));
tmp[sizeof(tmp) - 1] = '\0';

On notera l'ajout explicite du caractère nul, si ma_chaine est plus grande que la zone mémoire tmp. La fonction strncpy ne rajoutant hélas pas, dans ce cas, de caractère nul. C'est un problème tellement classique que toute application C reprogramme en général la fonction strncpy pour prendre en compte ce cas de figure (voir la fonction POSIX strdup, ou strlcopy utilisée par le système d'exploitation OpenBSD, par exemple)

La limitation d'une chaîne à un nombre d'octets pose par contre des problèmes dans le cas des caractères multi-octets qui risquent alors e ne pas être entiers[2].

Le langage n'offrant que très peu d'aide, la gestion correcte des chaînes de caractères est un problème à ne pas sous-estimer en C.

Recherche dans une chaîne

char * strchr(const char * chaine, int caractère);
char * strrchr(const char * chaine, int caractère);

Recherche le caractère dans la chaine et renvoie la position de la première occurrence dans le cas de strchr et la position de la dernière occurrence dans le cas de strrchr.

char * strstr(const char * meule_de_foin, const char * aiguille);

Recherche l'aiguille dans la meule de foin et renvoie la position de la première occurrence.

Nota: Dans la cas de caractères multi-octets, cette fonction ne cherche qu'un morceau de caractère [2].

Traitement des blocs mémoire

La bibliothèque <string.h> contient encore quelques fonctions pour la manipulation de zone brute de mémoire. Ces fonctions, préfixées par mem, sont les équivalents des fonctions str* pour des zones de mémoire qui ne sont pas des chaînes de caractères. Cela peut être des tableaux non terminés par la valeur 0, comme des tableaux pouvant contenir la valeur 0 avant la fin, par exemple. Elles permettent aussi de traiter des structures (par exemple pour copier les données d'une structure dans une autre), ou des zones de mémoires allouées dynamiquement (par exemple pour initialiser une zone mémoire allouée par malloc).

Comme, au contraire des fonctions str*, elles n'ont pas de délimiteur de fin de tableau, il faut leur donner en paramètre la taille de ces tableaux (cette taille étant en bytes au sens strict, bien que souvent en octets).

void memcpy( void * destination, const void * source, size_t longueur );

Copie 'longueur' octet(s) de la zone mémoire 'source' dans 'destination'. Vous devez bien sûr vous assurer que la chaine destination ait suffisament de place, ce qui est en général plus simple dans la mesure où l'on connait la longueur.
Attention au récouvrement des zones : si destination + longueur < source alors source >= destination.

void memmove( void * destination, const void * source, size_t longueur )

Identique à la fonction memcpy(), mais permet de s'affranchir totalement de la limitation de recouvrement.

void memset( void * memoire, int caractere, size_t longueur );

Initialise les 'longueur' premiers octets du bloc 'memoire', par la valeur convertie en type char de 'caractere'. Cette fonction est souvent employée pour mettre à zéro tous les champs d'une structure ou d'un tableau :

struct MonType_t mem;

memset( &mem, 0, sizeof mem );

int memcmp( const void * mem1, const void * mem2, size_t longueur );

Compare les 'longueur' premiers octets des blocs 'mem1' et 'mem2'. Renvoie les codes suivants :

  • < 0 : mem1 < mem2
  • = 0 : mem1 == mem2
  • > 0 : mem1 > mem2
void * memchr( const void * memoire, int caractere, size_t longueur );

Recherche dans les 'longueur' premiers octets du bloc 'memoire', la valeur convertie en type char de 'caractere'. Renvoie un pointeur sur l'emplacement où le caractère a été trouvé, ou NULL si rien n'a été trouvé.

void * memrchr( const void * memoire, int caractere, size_t longueur );

Pareil que memchr(), mais commence par la fin du bloc 'memoire'.

Références

Entrées/sorties

Les fonctions d'entrées/sorties sont celles qui vous permettent de communiquer avec l'extérieur, c'est-à-dire, la console, les fichiers, tubes de communication, socket IP, etc ... Pour utiliser ces fonctions, il faut inclure l'en-tête <stdio.h>, avec la directive d'inclusion :

#include <stdio.h>

Manipulation de fichiers

En C, les fichiers ouverts sont représentés par le type FILE, qui est un type opaque : on ne connaît pas la nature réelle du type, mais seulement des fonctions pour le manipuler. Ainsi, on ne peut créer directement de variables de type FILE, seules les fonctions de la bibliothèque standard peuvent créer une variable de ce type, lors de l'ouverture d'un fichier. Ces données sont donc uniquement manipulées par des pointeurs de type FILE *.

Ce type est un flux de données, qui représente des fichiers, mais peut aussi représenter toute autre source ou destination de données. L'en-tête <stdio.h> fournit trois flux que l'on peut utiliser « directement » :

  • stdin, l'entrée standard ;
  • stdout, la sortie standard ;
  • stderr, la sortie standard des erreurs.

Souvent, l'entrée standard envoie au programme les données issues du clavier, et les sorties standard envoient les données que le programme génère à l'écran. Mais d'où viennent et où vont ces données dépend étroitement du contexte et de l'implémentation ; la bibliothèque standard fournit le type FILE comme une abstraction pour les manipuler tous de la même manière, ce qui libère le programmeur de certains détails d'implémentations, et permet à l'utilisateur d'un programme d'employer (suivant son implémentation) aussi bien son clavier qu'un fichier comme entrée standard.

Ouverture

FILE * fopen(const char * restrict chemin, const char * restrict mode)

Ouvre le fichier désigné par le chemin et renvoie un nouveau flux de données pointant sur ce fichier. L'argument mode est une chaîne de caractères désignant la manière dont on veut ouvrir le fichier :

  • r : ouvre le fichier en lecture, le flux est positionné au début du fichier ;
  • r+ : ouvre le fichier en lecture/écriture, le flux est positionné au début du fichier ;
  • w : ouvre le fichier en écriture, supprime toutes les données si le fichier existe et le crée sinon, le flux est positionné au début du fichier ;
  • w+ : ouvre le fichier en lecture/écriture, supprime toutes les données si le fichier existe et le crée sinon, le flux est positionné au début du fichier ;
  • a : ouvre le fichier en écriture, crée le fichier s'il n'existe pas, le flux est positionné à la fin du fichier ;
  • a+ : ouvre le fichier en lecture/écriture, crée le fichier s'il n'existe pas, le flux est positionné à la fin du fichier.
Résumé des modes
mode | lecture | écriture | crée le fichier | vide le fichier | position du flux
r | X |  |  |  | début
r+ | X | X |  |  | début
w |  | X | X | X | début
w+ | X | X | X | X | début
a |  | X | X |  | fin
a+ | X | X | X |  | fin

Lorsqu'un fichier est ouvert en écriture, les données qui sont envoyées dans le flux ne sont pas directement écrites sur le disque. Elles sont stockées dans un tampon, une zone mémoire de taille finie. Lorsque le tampon est plein, les données sont purgées (flush), elles sont écrites dans le fichier. Ce mécanisme permet de limiter les accès au système de fichiers et donc d'accélérer les opérations sur les fichiers.

À noter une particularité des systèmes Microsoft Windows, est de traiter différemment les fichiers textes, des fichiers binaires. Sur ces systèmes, le caractère de saut de ligne est en fait composé de deux caractères (CR, puis LF, de code ASCII respectif 13 et 10, ou '\r' et '\n' écrit sous forme de caractère C). Lorsqu'un fichier est ouvert en mode texte (mode par défaut), toute séquence CRLF lue depuis le fichier sera convertie en LF, et tout caractère LF écrit sera en fait précédé d'un caractère CR supplémentaire. Si le fichier est ouvert en mode binaire, aucune conversion n'aura lieu.

Ce genre de comportement issu d'un autre âge, est en fait bien plus agaçant que réellement utile. Le premier réflexe est en général de désactiver ce parasitage des entrées/sorties, tant il est pénible. Pour cela deux cas de figure :

  1. Vous avez accès au nom du fichier : donc vous pouvez utiliser la fonction fopen(). Dans ce cas de figure, il suffit de rajouter la lettre b au mode d'ouverture:
    fopen("fichier.txt", "rb"); /* Ouverture sans conversion */
fopen("fichier.txt", "wb"); /* L'écriture d'un '\n' n'entrainera pas l'ajout d'un '\r' */
  2. Vous n'avez pas accès au nom du fichier (par exemple stdout ou stdin). Il existe une fonction spécifique à Microsoft Windows, non portable sur d'autres systèmes, mais qui peut s'avérer nécessaire:
    /* Ce code qui suit n'est pas portable */
#include <fcntl.h>

setmode(descripteur, O_BINARY);

    Histoire de compliquer encore un petit peu, cette fonction travaille avec les descripteurs de fichier, plutôt que les flux stdio. On peut tout de même s'en sortir avec cette fonction faisant partie de la bibliothèque standard:

    int fileno(FILE *);

    Par exemple pour mettre la sortie standard en mode binaire:

    setmode(fileno(stdout), O_BINARY);

Fermeture

int fclose(FILE * flux);

Dissocie le flux du fichier auquel il avait été associé par fopen. Si le fichier était ouvert en écriture, le tampon est vidé. Cette fonction renvoie 0 si la fermeture s'est bien passée (notamment la purge des zones en écriture), ou EOF en cas d'erreur (voir le paragraphe sur la gestion d'erreurs).

Suppression

int remove(const char * path);

Supprime le fichier ou le répertoire nommé 'path'. La fonction renvoie 0 en cas de réussite et une valeur non nulle en cas d'erreur, ce qui peut inclure :

  • un répertoire n'est pas vide ;
  • vous n'avez pas les permissions pour effacer le fichier (média en lecture seule) ;
  • le fichier est ouvert ;
  • etc.

Renommage (ou déplacement)

int rename(const char * ancien_nom, const char * nouveau_nom);

Cette fonction permet de renommer l'ancien fichier ou répertoire nommé 'ancien_nom' par 'nouveau_nom'. Elle peut aussi servir a déplacer un fichier, en mettant le chemin absolu ou relatif du nouvel emplacement dans 'nouveau_nom'.

La fonction renvoie 0 si elle réussie et une valeur non nulle en cas d'erreur.

Les causes d'erreur dépendent de l'implémentation, et peuvent être:

  • vous tentez d'écraser un répertoire par un fichier ;
  • vous voulez écraser un répertoire non vide ;
  • vous n'avez pas les permissions suffisantes ;
  • les deux noms ne sont pas sur la même partition ;
  • etc.

Déplacement dans le flux

int fseek( FILE * flux, long deplacement, int methode );
long ftell( FILE * flux );

fseek permet de se déplacer à une position arbitraire dans un flux. Cette fonction renvoie 0 en cas de réussite.

deplacement indique le nombre d'octet à avancer (ou reculer si ce nombre est négatif) à partir du point de référence (methode) :

  • SEEK_SET : le point de référence sera le début du fichier.
  • SEEK_CUR : le point de référence sera la position courante dans le fichier.
  • SEEK_END : le point de référence sera la fin du fichier.

ftell permet de savoir à quelle position se trouve le curseur (ce depuis le début).

En cas d'erreur, ces deux fonctions renvoient -1.

Plusieurs remarques peuvent être faites sur ces deux fonctions :

  1. Sur une machine 32bits pouvant gérer des fichiers d'une taille de 64bits (plus de 4Go), ces fonctions sont limite inutilisables, du fait qu'un type long sur une telle architecture est limité à 32bits. On mentionnera les fonctions fseeko() et ftello() qui utilisent le type opaque off_t, à la place du type int, à l'image des appels systèmes. Ce type off_t est codé sur 64bits sur les architectures le supportant et 32bits sinon. La disponibilité de ces fonctions est en général limités aux systèmes Unix, puisque dépendantes de la spécification Single Unix (SUS).
  2. Il faut bien sûr que le périphérique où se trouve le fichier supporte une telle opération. Dans la terminologie Unix, on appelle cela un périphérique en mode bloc. À la différence des périphériques en mode caractère (stdin, stdout, tube de communication, connexion réseau, etc ...) pour lesquels ces appels échoueront.

Synchronisation

int fflush ( FILE *flux );

Cette fonction purge toutes les zones mémoires en attente d'écriture et renvoie 0 si tout s'est bien passé, ou EOF en cas d'erreur. Si NULL est passé comme argument, tous les flux ouverts en écriture seront purgés.

À noter que cette fonction ne permet pas de purger les flux ouverts en lecture (Pour répondre à une question du genre « Voulez-vous effacer ce fichier (o/n) ? » ). Une instruction de ce genre sera au mieux ignorée, et au pire provoquera un comportement indéterminé :

Avertissement Ce code contient une erreur volontaire !
fflush( stdin );

Pour effectuer une purge des flux ouverts en lecture, il faut passer par des appels systèmes normalisés dans d'autres documents (POSIX), mais dont la disponibilité est en général dépendante du système d'exploitation.

Sorties formatées

int printf(const char * restrict format, ...);
int fprintf(FILE * restrict flux, const char * restrict format, ...);
int sprintf(char * restrict chaine, const char * restrict format, ...);
int snprintf(char * restrict chaine, size_t taille, const char * restrict format, ...);

Ces fonctions permettent d'écrire des données formatées dans :

  • la sortie standard pour printf ;
  • un flux pour fprintf ;
  • une chaîne de caractères pour sprintf.

En retour elle indique le nombre de caractères qui a été écrit à l'écran, dans le flux ou la zone mémoire (caractère nul non compris pour sprintf).

Bien que cela ait déjà été traité dans la section dédiée aux chaines de caractères, il faut faire très attention avec la fonction sprintf(). Dans la mesure où la fonction n'a aucune idée de la taille de la zone mémoire transmise, il faut s'assurer qu'il n'y aura pas de débordements. Mieux vaut donc utiliser la fonction snprintf(), qui permet de limiter explicitement le nombre de caractère à écrire.

À noter que snprintf() devrait toujours retourner la taille de la chaine à écrire, indépendamment de la limite fixée par le paramètre taille. Le conditionnel reste de mise, car beaucoup d'implémentations de cette fonction se limitent à retourner le nombre de caractères écrit, c'est à dire en s'arrêtant à la limite le cas échéant.

Type de conversion

Mis à part l'« endroit » où écrivent les fonctions, elles fonctionnent exactement de la même manière, nous allons donc décrire leur fonctionnement en prenant l'exemple de printf.

L'argument format est une chaîne de caractères qui détermine ce qui sera affiché par printf et sous quelle forme. Cette chaîne est composée de texte « normal » et de séquences de contrôle permettant d'inclure des variables dans la sortie. Les séquences de contrôle commencent par le caractère « % » suivi d'un caractère parmi :

  • d ou i pour afficher un entier signé au format décimal (int) ;
  • u pour un entier non signé au format décimal ;
  • x ou X pour afficher un entier au format hexadécimal (avec les lettres "abcdef" pour le format 'x' et "ABCDEF" avec le format 'X') ;
  • f pour afficher un réel (double) avec une précision fixe ;
  • e pour afficher un réel (double) en notation scientifique ;
  • g effectue un mixe de 'f' et de 'e' suivant le format le plus approprié ;
  • c pour afficher en tant que caractère ;
  • s pour afficher une chaine de caractère C standard ;
  • p pour afficher la valeur d'un pointeur, généralement sous forme hexadécimale. Suivant le compilateur, c'est l'équivalent soit à "%08x", ou alors à "0x%08x". ;
  • n ce n'est pas un format d'affichage et l'argument associé doit être de type int * et être une référence valide. La fonction stockera dans l'entier pointé par l'argument le nombre de caractères écrit jusqu'à maintenant ;
  • % pour afficher le caractère '%'.

Contraindre la largeur des champs

Une autre fonctionnalité intéressante du spécificateur de format est que l'on peut spécifier sur combien de caractère les champs seront alignés. Cette option se place entre le '%' et le format de conversion et se compose d'un signe '-' optionnel suivit d'un nombre, éventuellement d'un point et d'un autre nombre ([-]<nombre>[.<nombre>]). Par exemple: %-30.30s.

Le premier nombre indique sur combien de caractère se fera l'alignement. Si la valeur convertie est plus petite, elle sera alignée sur la droite, ou la gauche si un signe moins est présent au début. Si la valeur est plus grande que la largeur spécifiée, le contenu s'étendra au-delà, décalant tout l'alignement. Pour éviter ça, on peut spécifier un deuxième nombre au delà duquel le contenu sera tronqué. Quelques exemples:

printf("%10s",    "Salut");               /* " Salut" */
printf("%-10s",   "Salut");               /* "Salut " */
printf("%10s",    "Salut tout le monde"); /* "Salut tout le monde" */
printf("%10.10s", "Salut tout le monde"); /* "Salut tout" */

Contraindre la largeur des champs numériques

On peut aussi paramétrer la largeur du champ, en spécifiant * à la place. Dans ce cas, en plus de la valeur à afficher (i.e. 1234), il faut donner avant un entier de type int pour dire sur combien de caractères l'alignement se fera (i.e. 10) :

printf("%-*d", 10, 1234); /* "1234 " */
printf("%*d",  10, 1234); /* " 1234" */

À noter que pour le formattage de nombres entiers, la limite « dure » du spécificateur de format est sans effet, pour éviter de facheuses erreurs d'interprétation. On peut toutefois utiliser les extensions suivantes :

  • 0 : Si un zéro est présent dans le spécificateur de largeur, le nombre sera aligné avec zéros au lieu de blancs.
  • + : Si un signe plus est présent avec le spécificateur de largeur, le signe du nombre sera affiché tout le temps (0 est considéré comme positif).
  •   (espace) : Si le nombre est positif, un blanc sera mis avant, pour l'aligner avec les nombres négatifs.

Exemples :

printf("%+010d",    543);        /* "+000000543"  */
printf("%-+10d",    543);        /* "+543 "       */
printf("%-+10d",    1234567890); /* "+1234567890" */
printf("%-+10.10d", 1234567890); /* "+1234567890" */
printf("%08x",      543);        /* "0000021f"    */

Contraindre la largeur des champs réels

Pour les réels, la limite « dure » sert en fait à indiquer la précision voulue après la virgule :

printf("%f",   3.1415926535); /* "3.141593"   */
printf("%.8f", 3.1415926535); /* "3.14159265" */

Spécifier la taille de l'objet

Par défaut, les entiers sont présuposés être de type int, les réels de type double et les chaines de caractères de type char *. Il arrive toutefois que les types soient plus grands (et non plus petits à cause de la promotion des types, c.f. paragraphes opérateurs et fonction à nombre variable d'arguments), le spécificateur de format permet donc d'indiquer la taille de l'objet en ajoutant les attributs suivants avant le caractère de conversion :

  • hh : indique que l'entier est un [un]signed char au lieu d'un [unsigned] int ;
  • h : indique que l'entier est de type [unsigned] short au lieu d'un [unsigned] int ;
  • l : pour les entiers, le type attendu ne sera plus int mais long int et pour les chaines de caractères, il sera de type wchar_t * (c.f section chaînes de caractères).
  • ll : cet attribut ne concerne que les types entiers, où le type attendu sera long long int.
  • L : pour les types réels, le type attendu sera long double.
  • z pour afficher une variable de type size_t.

Pour résumer les types d'arguments attendus en fonction de l'indicateur de taille et du type de conversion :

Format | Attributs de taille |  |  |  |  |  | Autres attributs (rarement utilisés)
 | aucun | hh | h | l (elle) | ll (elle-elle) | L | j | z | t
n | int * | signed char * | short * | long * | long long * |  | intmax_t * | size_t * | ptrdiff_t *
d, i, o, x, X | int | signed char | short | long | long long |  | intmax_t | size_t | ptrdiff_t
u | unsigned int | unsigned char | unsigned short | unsigned long | unsigned long long |  | uintmax_t | size_t | ptrdiff_t
s | char * |  |  | wchar_t * |  |  |  |  |
c | int |  |  | wint_t |  |  |  |  |
p | void * |  |  |  |  |  |  |  |
a, A, e, E, f, F, g, G | double |  |  |  |  | long double |  |  |

hh et ll sont des nouveautés de C99. On notera qu'avec l'attribut hh et les formats n, d, i, o, x ou X, le type est signed char et non char. En effet, comme vu dans le chapitre Types de base, le type char peut être signé ou non, suivant l'implémentation. Ici, on est sûr de manipuler le type caractère signé.

Quelques exemples :

signed char nb;

printf("%d%hhn", 12345, &nb); /* Affichage de "12345" et nb vaudra 5 */
printf("%ls", L"Hello world!"); /* "Hello world!" */

Arguments positionnels

Il s'agit d'une fonctionnalité relativement peu utilisée, mais qui peut s'avérer très utile dans le cadre d'une application internationnalisée. Considérez le code suivant (tiré du manuel de gettext) :

printf( gettext("La chaine '%s' a %zu caractères\n"), s, strlen(s) );

gettext est un ensemble de fonctions permettant de manipuler des catalogues de langues. La principale fonction de cette bibliothèque est justement gettext(), qui en fonction d'une chaine de caractère retourne la chaine traduite selon la locale en cours (où celle passée en argument si rien n'a été trouvé).

Une traduction en allemand du message précédant, pourrait donner : "%d Zeichen lang ist die Zeichenkette '%s'"

On remarque d'emblée que les spécificateurs de format sont inversés par rapport à la chaine originale. Or l'ordre des arguments passés à la fonction printf() sera toujours le même. Il est quand même possible de s'en sortir avec les arguments positionnels. Pour cela, il suffit d'ajouter à la suite du caractère % un nombre, suivi d'un signe $. Ce nombre représente le numéro de l'argument à utiliser pour le spécificateur, en commençant à partir de 1. Un petit exemple :

char * s = "Bla bla";
printf("La chaine %2$s a %1$zu caractères\n", strlen(s), s); /* "La chaine Bla bla a 7 caractères" */

À noter que si un des arguments utilise la référence positionnelle, tous les autres arguments devront faire évidemment de même, sous peine d'avoir un comportement imprévisible.

Écriture par bloc ou par ligne

Il s'agit d'une fonctionnalité relativement pointue de la bibliothèque stdio, mais qui peut expliquer certains comportement en apparence étrange (notamment avec les systèmes POSIX). Les réglages par défaut étant bien faits, il y a peu de chance pour que vous ayez à vous soucier de cet aspect, si ce n'est à titre de curiosité.

En règle générale les flux de sortie ouvert par via la bibliothèque stdio sont gérés par bloc, ce qui veut dire qu'une écriture (via printf(), fprintf() ou fwrite()) ne sera pas systématiquement répercutée dans le fichier associé.

Cela dépend en fait du type d'objet sur lequel les écritures se font :

  • Un terminal : les écritures se feront par ligne, ou si les lignes sont plus grandes qu'une certaine taille (4Ko en général), l'écriture se fera par bloc. Les flux en écriture seront aussi purgés si on tente de lire des données depuis le même terminal.
  • Autre (fichiers, connexion réseau, tubes de communication) : les écritures se feront par bloc, indépendamment des lignes.
  • Flux d'erreur (stderr) : écriture immédiate.

C'est ce qui fait qu'un programme affichant des messages à intervalle régulier (genre une seconde), affichent ces lignes une à une sur un terminal, et par bloc de plusieurs lignes lorsqu'on redirige sa sortie vers un programme de mise en page (comme more), avec une latence qui peut s'avérer génante. C'est ce qui fait aussi qu'une instruction comme printf("Salut tout le monde"); n'affichera en général rien, car il n'y a pas de retour à la ligne.

En fait ce comportement peut être explicitement réglé, avec cette fonction :

int setvbuf(FILE * restrict flux, char * restrict mem, int mode, size_t taille);

Cette fonction doit être appelée juste après l'ouverture du flux et avant la première écriture. Les arguments ont la signification suivante :

  • flux : Le flux stdio pour lequel vous voulez changer la méthode d'écriture.
  • mem : Vous pouvez transmettre une zone mémoire qui sera utilisée pour stocker les données avant d'être écrites dans le fichier. Vous pouvez aussi passer la valeur NULL, dans ce cas les fonctions stdio, appelleront la fonction malloc() lors de la première écriture.
  • mode : indique comment se feront les écritures :
    • _IONBF : écriture immédiate, pas de stockage temporaire.
    • _IOLBF : écriture par ligne.
    • _IOFBF : par bloc.
  • taille : La taille de la zone mémoire transmise ou à allouer.

La fonction setvbuf() renvoie 0 si elle réussi, et une valeur différente de zéro dans le cas contraire (en général le paramètre mode est invalide).

Cette fonctionnalité peut être intéressante pour les programmes générant des messages sporadiques. Il peut effectivement s'écouler un temps arbitrairement long avant que le bloc mémoire soit plein, si cette commande est redirigée vers un autre programme, ce qui peut s'avérer assez dramatique pour des messages signalant une avarie grave. Dans ce cas, il est préférable de forcer l'écriture par ligne (ou immédiate), plutôt que de faire suivre systématiquement chaque écriture de ligne par un appel à fflush(), avec tous les risques d'oubli que cela comporte.

Quelques remarques pour finir

La famille de fonctions printf() permet donc de couvrir un large éventail de besoins, au prix d'une pléthore d'options pas toujours faciles à retenir.

Il faut aussi faire attention au fait que certaines implémentations de printf() tiennent compte de la localisation pour les conversions des nombres réels (virgule ou point comme séparateur décimal, espace ou point comme séparateurs des milliers, etc.). Ceci peut être gènant lorsqu'on veut retraiter la sortie de la commande. Pour désactiver la localisation, on peut utiliser la fonction setlocale():

#include <locale.h>

/* ... */
setlocale( LC_ALL, "C" );
printf( ... );
setlocale( LC_ALL, "" );

Entrées formatées

La bibliothèque stdio propose quelques fonctions très puissantes pour saisir des données depuis un flux quelconque. Le comportement de certaines fonctions (scanf notamment) peut paraitre surprenant de prime abord, mais s'éclaircira à la lumière des explications suivantes.

int scanf(const char * restrict format, ...);
int fscanf(FILE * restrict flux, const char * restrict format, ...);
int sscanf(const char * restrict chaine, const char * restrict format, ...);

Ces trois fonctions permettent de lire des données formatées provenant de :

  • l'entrée standard pour scanf ;
  • un flux pour fscanf ;
  • une chaîne de caractères pour sscanf.

L'argument format ressemble aux règles d'écriture de la famille de fonction printf, cependant les arguments qui suivent ne sont plus des variables d'entrée mais des variables de sortie (ie : l'appel à scanf va modifier leur valeur, il faut donc passer une référence).

Ces fonctions retournent le nombre d'arguments correctement lus depuis le format, qui peut être inférieur ou égal au nombre de spécificateurs de format, et même nul.

Format de conversion

Les fonctions scanf() analysent le spécificateur de format et les données d'entrée, en les comparant caractère à caractère et s'arrêtant lorsqu'il y en a un qui ne correspond pas. À noter que les blancs (espaces, tabulations et retour à la ligne) dans le spécificateur de format ont une signification spéciale : à un blanc de la chaine format peut correspondre un nombre quelconque de blanc dans les données d'entrée, y compris aucun. D'autres part, il est possible d'insérer des séquences spéciales, commençant par le caractère '%' et à l'image de printf(), pour indiquer qu'on aimerait récupérer la valeur sous la forme décrite par le caractère suivant le '%' :

  • s : extrait la chaîne de caractères, en ignorant les blancs initiaux et ce jusqu'au prochain blanc. L'argument correspondant doit être de type char * et pointer vers un bloc mémoire suffisamment grand pour contenir la chaîne et son caractère terminal.
  • d : extrait un nombre décimal signé de type int, ignorant les espaces se trouvant éventuellement avant le nombre.
  • i : extrait un nombre (de type int) hexadécimal, si la chaine commence par "0x", octal si la chaine commence par "0" et décimal sinon. Les éventuels espaces initiaux seront ignorés.
  • f : extrait un nombre réel, en sautant les blancs, de type float.
  • u : lit un nombre décimal non-signé, sans les blancs, de type int.
  • c : lit un caractère (de type char), y compris un blanc.
  • [] : lit une chaîne de caractères qui doit faire partie de l'ensemble entre crochets. Cet ensemble est une énumération de caractère. On peut utiliser le tiret ('-') pour grouper les déclarations (comme "0-9" ou "a-z"). Pour utiliser le caractère spécial ']' dans l'ensemble, il doit être placé en première position et, pour utiliser le tiret comme un caractère normal, il doit être mis à la fin. Pour indiquer que l'on veut tous les caractères sauf ceux de l'ensemble, on peut utiliser le caractère '^' en première position. À noter que scanf terminera toujours la chaîne par 0 et que, contrairement au spécificateur %s, les blancs ne seront pas ignorés.
  • n : Comme pour la fonction printf(), ce spécificateur de format permet de stocker dans l'entier correspondand de type int, le nombre de caractères lus jusqu'à présent.

Contraindre la largeur

Comme pour la fonction printf(), il est possible de contraindre le nombre de caractères à lire, en ajoutant ce nombre juste avant le caractère de conversion. Dans le cas des chaînes, c'est même une obligation, dans la mesure où scanf() ne pourra pas ajuster l'espace à la volée.

Exemple :

/* Lit une chaîne de caractères entre guillemets d'au plus 127 caractères */
char tmp[128];

if (fscanf(fichier, "Config = \"%127[^\"]\"", tmp ) == 1)
{
    printf("L'argument associé au mot clé 'Config' est '%s'\n", tmp);
}

Cet exemple est plus subtil qu'il ne paraît. Il montre comment analyser une structure relativement classique de ce qui pourrait être un fichier de configuration de type "MotClé=Valeur". Ce format spécifie donc qu'on s'attend à trouver le mot clé "Config", en ignorant éventuellement les blancs initiaux, puis le caractère '=', entouré d'un nombre quelconque de blancs, éventuellement aucun. À la suite de cela, on doit avoir un guillemet ('"'), puis au plus 127 caractères autres que que les guillemets, qui seront stockés dans la zone mémoire tmp (qui sera terminée par 0, d'où l'allocation d'un caractère supplémentaire). Le guillemet final est là pour s'assurer, d'une part, que la longueur de la chaîne est bien inférieure à 127 caractère et, d'autre part, que le guillemet n'a pas été oublié dans le fichier.

En cas d'erreur, on peut par exemple ignorer tous les caractères jusqu'à la ligne suivante.

Ajuster le type des arguments

On peut aussi ajuster le type des arguments en fonction des attributs de taille :

Format | Attributs de taille
 | aucun | hh | h | l (elle) | ll (elle-elle)
d, i, n | int * | char * | short * | long * | long long *
u | unsigned int * | unsigned char * | unsigned short * | unsigned long * | unsigned long long *
s, c, [ ] | char * |  |  |  |
f | float * |  |  | double * | long double *

Ainsi pour lire la valeur d'un entier sur l'entrée standard, on utilisera un code tel que celui ci :

#include <stdio.h>

int main(void)
{
    int i;

    printf("Entrez un entier : ");
    scanf("%d", &i);
    printf("la variable i vaut maintenant %d\n", i);
    return 0;
}

Les appels à printf ne sont pas indispensables à l'exécution du scanf, mais permettent à l'utilisateur de comprendre ce qu'attend le programme (et ce qu'il fait aussi).

Conversions muettes

La fonction scanf reconnait encore un autre attribut qui permet d'effectuer la conversion, mais sans retourner la valeur dans une variable. Il s'agit du caractère étoile '*', qui remplace l'éventuel attribut de taille.

En théorie, la valeur de retour ne devrait pas tenir compte des conversions muettes.

Exemple:

int i, j;

sscanf("1 2.434e-308 2", "%d %*f %d", &i, &j); /* i vaut 1 et j vaut 2 */

Quelques remarques pour finir

La fonction scanf() n'est pas particulièrement adaptée pour offrir une saisie conviviale à l'utilisateur, même en peaufinant à l'extrême les spécificateurs de format. En général, il faut s'attendre à une gestion très rudimentaire du clavier, avec très peu d'espoir d'avoir ne serait-ce que les touches directionnelles pour insérer du texte à un endroit arbitraire.

Qui plus est, lors de saisie de texte, les terminaux fonctionnent en mode bloc : pour que les données soient transmises à la fonction de lecture, il faut que l'utilisateur confirme sa saisie par entrée. Même les cas les plus simple peuvent poser problèmes. Par exemple, il arrive souvent qu'on ne veuille saisir qu'un caractère pour répondre à une question du genre "Écraser/Annuler/Arrêter ? (o|c|s)", avant d'écraser un fichier. Utiliser une des fonctions de saisie nécessite de saisir d'abord un caractère, ensuite de valider avec la touche entrée. Ce qui peut non seulement être très pénible s'il y a beaucoup de questions, mais aussi risqué si on ne lit les caractères qu'un à un. En effet, dans ce dernier cas, si l'utilisateur entre une chaine de plusieurs caractères, puis valide sa saisie, les caractères non lus seront disponibles pour des lectures ultérieures, répondant de ce fait automatiquement aux questions du même type.

Il s'agit en fait de deux opérations en apparence simples, mais impossible à réaliser avec la bibliothèque C standard :

  1. Purger les données en attente de lecture, pour éviter les réponses « automatiques ».
  2. Saisir des caractères sans demande de confirmation.

Ces fonctionnalités sont hélas le domaine de la gestion des terminaux POSIX et spécifiées dans la norme du même nom.

On l'aura compris, cette famille de fonction est plus à l'aise pour traiter des fichiers, ou tout objet nécessitant le moins d'interaction possible avec l'utilisateur. Néanmoins dans les cas les plus simples, ce sera toujours un pis-aller.

À noter, que contrairement à la famille de fonction printf(), scanf() n'est pas sensible à la localisation pour la saisie de nombres réels.

Entrées non formatées

Pour saisir des chaines de caractères indépendemment de leur contenu, on peut utiliser les fonctions suivantes :

char * fgets(char * restrict chaine, int taille_max, FILE * restrict flux);
int fgetc( FILE * restrict flux);
int ungetc( int octet, FILE * flux );

La fonction fgets() permet de saisir une ligne complète dans la zone mémoire spécifiée, en évitant tout débordement. Si la ligne peut être contenue dans le bloc, elle contiendra le caractère de saut de ligne ('\n'), en plus du caractère nul. Dans le cas contraire, la ligne sera tronquée, et la suite de la ligne sera obtenue à l'appel suivant. Si la fonction a pu lire au moins un caractère, elle retournera la chaine transmise en premier argument, ou NULL s'il n'y a plus rien à lire.

Un exemple de lecture de ligne arbitrairement longue est fournie dans le livre Exercices en langage C (énnoncé et solution).

La fonction fgetc() permet de ne saisir qu'un caractère depuis le flux spécifié. À noter que la fonction renvoie bien un entier de type int et non de type char, car en cas d'erreur (y compris la fin de fichier), cette fonction renvoie EOF (défini à -1 en général).

À noter que cette fonction est incapable de traiter des fichiers mixtes (binaire et texte) depuis un descripteur en mode caractère (accès séquentiel). D'une part la fonction ne renvoyant pas le nombre d'octets lus (ce qui aurait facilement réglé le problème) et d'autre part, ftell() ne fonctionnant pas sur de tels descripteurs, il faudra reprogrammer fgets pour gérer ce cas de figure.

Entrées/sorties brutes

Les fonctions suivantes permettent d'écrire ou de lire des quantités arbitraires de données depuis un flux. Il faut faire attention à la portabilité de ces opérations, notamment lorsque le flux est un fichier. Dans la mesure où lire et écrire des structures binaires depuis un fichier nécessite de gérer l'alignement, le bourrage, l'ordre des octets pour les entiers (big endian, little endian) et le format pour les réels, il est souvent infiniment plus simple de passer par un format texte.

Sortie

size_t fwrite(const void * buffer, size_t taille, size_t nombre, FILE * flux);

Entrée

size_t fread(void * buffer, size_t taille, size_t nombre, FILE * flux);

Lit nombre éléments, chacun de taille taille, à partir du flux et stocke le résultat dans le buffer. Renvoie le nombre d'éléments correctement lus.

Gestion des erreurs

Qui dit entrées/sorties dit forcément une pléthore de cas d'erreurs à gérer. C'est souvent à ce niveau que se distinguent les « bonnes » applications des autres : fournir un comportement cohérent face à ces situations exceptionnelles. Dans la description des fonctions précédentes, il est fait mention que, en cas d'erreur, un code spécial est retourné par la fonction. De plus, on dispose de la fonction int ferror( FILE * );, qui permet de savoir si une erreur a été déclenchée sur un fichier lors d'un appel antérieur à une fonction de la bibliothèque standard.

Si ces informations permettent de savoir s'il y a eu une erreur, elles ne suffisent pas à connaître la cause de l'erreur. Pour cela, la bibliothèque stdio repose sur la variable globale errno, dont l'utilisation est décrite dans le chapitre sur la gestion d'erreur.

Erreurs

Le langage C fournit un en-tête spécialisé pour la gestion des erreurs : <errno.h>. Cet en-tête déclare notamment une variable globale errno, et un certain nombre de codes d'erreur, qui permettent aux fonctions de la bibliothèque standard de reporter précisément la cause d'une erreur.

Utilisation

Pour inclure l'en-tête de son fichier source, il faut ajouter la ligne :

#include <errno.h>

On peut alors utiliser la variable errno, de type int, pour traiter les erreurs[3].

Lorsqu'on veut utiliser errno pour déterminer la cause d'un échec, il faut d'abord s'assurer que la fonction a bel et bien échoué. Le C laissant une certaine liberté dans la manière de signaler un échec, il n'y a pratiquement aucun mécanisme universel qui permette de détecter une telle situation, chaque fonction étant presque un cas particulier. Cependant, une pratique relativement répandue est de retourner un code spécifique, en général en dehors de l'intervalle de ce qu'on attend. Par exemple, lorsque la fonction est censée allouer un objet et retourne un pointeur, une erreur est souvent signalée en retournant un pointeur nul, et la variable errno décrit plus en détail la nature de l'erreur.

Il est nécessaire de placer errno à 0 avant d'utiliser la fonction qui peut échouer, car les fonctions de la bibliothèque standard ne sont pas obligées de la mettre à zéro en cas de succès. Si on ne la réinitialisait pas « manuellement », on pourrait « voir » le résultat d'une erreur causée par un appel de fonction antérieur. La procédure à suivre est donc la suivante :

  • Mettre errno à 0 ;
  • Appeler la fonction (de la bibliothèque standard) que l'on souhaite ;
  • Vérifier si elle a échoué ;
    • Si c'est le cas : la valeur de errno est disponible pour traiter l'erreur ;
    • Sinon : procéder à la suite du traitement.

Affichage d'un message d'erreur

Une fois qu'on est sûr que la variable errno contient une valeur pertinente, il est indispensable de déterminer le traitement à effectuer, et le cas échéant présenter un message tout aussi significatif à l'utilisateur. Rien n'est plus frustrant pour un utilisateur qu'un message aussi abscons que : "fopen("/tmp/tmpab560f9d4", "w") : erreur 28", alors qu'un message écrit sous la forme "impossible de créer le fichier /tmp/tmpab560f9d4: plus de place disponible sur le périphérique" sera plus facilement compris par l'utilisateur.

Énumérer toutes les valeurs possibles d'errno pour leur associer un message peut être relativement pénible, surtout si on doit le faire dans chaque application. Heureusement qu'une fonction toute prête existe :

#include <string.h>
char * strerror(int code);

Cette fonction permet de connaître la signification textuelle d'une valeur de errno. À noter que le code de retour est une chaine statique, dont il est sage de présupposer la durée de vie la plus courte possible. N'essayez pas non plus de modifier le contenu de la chaîne, affichez-la directement ou copiez-la dans une zone temporaire.

Un autre intérêt de passer par cette fonction est que les messages retournés sont adaptés à la localisation du système. Ce qui est loin d'être négligeable, car cela retire au programmeur le souci de traduire les messages d'erreurs dus à la bibliothèque standard dans toutes les langues possibles (chaque implémentation pouvant avoir ses propres codes d'erreur spécifiques, la maintenance d'une telle traduction serait un cauchemar).

Une autre fonction, perror, permet d'envoyer le message correspondant à errno sur le flux des erreurs stderr.

#include <errno.h>
void perror(const char *chaine);

Le paramètre chaine, s'il est différent de NULL, est une chaine de caractère passée par le programmeur et affichée avant le message d'erreur.

Exemple

Pour illustrer cela, voici un exemple, qui tente de convertir une chaine de caractères en un nombre. Le nombre donné, 264, peut ne pas être dans le domaine de valeur du type unsigned long (qui peut se limiter à 232 - 1), suivant l'architecture. Dans ce cas, on utilise errno pour afficher un message d'erreur approprié à l'utilisateur.

#include <stdio.h>  /* puts(), printf(), NULL */
#include <stdlib.h> /* strtoul() */
#include <limits.h> /* ULONG_MAX */
#include <string.h> /* strerror() */
#include <errno.h>  /* errno */

static void teste(const char *nombre)
{
    unsigned long res;

    /* On réinitialise errno */
    errno = 0;

    /* Appel de strtoul : conversion d'une chaine en un entier unsigned long*/
    res = strtoul(nombre, NULL, 10);

    /* On détecte une erreur de strtoul quand elle renvoie ULONG_MAX _et_
     * que errno est non nul. En effet, si on lui passe en entrée la
     * représentation de ULONG_MAX, la conversion se fait sans erreur
     * et errno reste à 0.
     */
    if (res == ULONG_MAX && errno != 0)
    {
        /* Il y a eu une erreur ! */
        (void)fprintf(stderr, "Impossible de convertir le nombre '%s': %s.\n",
            nombre, strerror(errno));
    }
    else
    {
       (void)printf("La conversion a été effectuée, et la valeur est: %lu\n", res);
    }
}

int main(void)
{
    /* 2 puissance 8 : sera toujours accepte */
    teste("256");

    /* 2^64 - 1 : peut echouer suivant la machine */
    teste("18446744073709551615");
    return EXIT_SUCCESS;
}

Si le type unsigned long est codé sur 32 bits, on peut obtenir:

La conversion a été effectuée, et la valeur est: 256
Impossible de convertir le nombre '18446744073709551615': la valeur est en dehors de l'intervalle du type spécifié.

Notons bien qu'il est nécessaire de placer errno à 0 avant d'utiliser strtoul, car elle pourrait contenir une valeur non nulle, même au lancement du programme.

Mathématiques

Pour pouvoir utiliser les fonctions mathématiques, il faut utiliser l'en-tête <math.h>, ainsi que <errno.h> pour gérer les erreurs :

#include <math.h>
#include <errno.h>

Comme pour un certain nombre de fonctions de la bibliothèques standard, il est en effet nécessaire d'utiliser <errno.h> pour détecter l'erreur d'une fonction mathématique (voir le chapitre sur la gestion d'erreurs pour voir comment utiliser <errno.h>).

Note : Sur certains compilateurs comme GCC, il est nécessaire d'ajouter durant l'édition des liens une option pour que la « librairie mathématique » soit liée au programme. Pour GCC, cette option est -lm. Sans cette option, le programme pourra compiler, mais le résultat à l'exécution sera surprenant...

Exponentiations

double exp ( double x );
double pow ( double x, double y );

exp calcule e élevé à la puissance de x ( [image]) où e est la base des logarithmes naturels (ln(e) = 1). pow calcule la valeur de x élévé à la puissance y ( [image]).

Erreurs

La fonction pow peut déclencher l'erreur suivante :

  • EDOM : x est négatif, et y n’est pas un entier.

Logarithmes

double log ( double x );
double log10 ( double x );

log calcule le logarithme néperien de x (noté généralement ln(x) en mathématiques). log10 calcule le logarithme à base 10 de x.

Erreurs

  • EDOM : x est négatif ;
  • ERANGE : x est nul.

Racine carrée

double sqrt ( double x );

Renvoie la racine carrée de x.

Erreurs

  • EDOM : x est négatif.

Sinus, cosinus, tangente

double sin ( double x );
double cos ( double x );
double tan ( double x );

Note : les angles retournés sont en radians (intervalle -pi/2 à pi/2).

Arc sinus, arc cosinus

double asin ( double x );
double acos ( double x );

Erreurs

  • EDOM : x est inférieur à -1 ou supérieur à 1.

Arc tangente

double atan ( double x );
double atan2 ( double y, double x );

Gestion de la mémoire

La gestion dynamique de la mémoire en C se fait à l'aide de principalement deux fonctions de la bibliothèque standard :

  • malloc, pour l'allocation dynamique de mémoire ;
  • free, pour la libération de mémoire préalablement allouée avec malloc.

Deux autres fonctions permettent de gérer plus finement la mémoire :

  • calloc, pour allouer dynamiquement de la mémoire, comme malloc, qui a préalablement été initialisée à 0 ;
  • realloc, pour modifier la taille d'une zone mémoire déjà allouée.

Ces fonctions sont déclarées dans l'en-tête <stdlib.h>.

Gestion dynamique de la mémoire

Les déclarations de variables en C et dans beaucoup d'autres langages ont une limitation très importante : la taille des variables doit être connue à la compilation. Cela pose problème quand on ne sait qu'à l'exécution le nombre de données qu'on doit traiter. Pour résoudre ce problème, et pouvoir décider durant l'exécution d'un programme du nombre de variables à créer, il faudra nécessairement passer par de l'allocation dynamique de mémoire. Avec ce système, le programmeur dispose de fonctions qui permettent de demander au système une zone mémoire d'une certaine taille, qu'il pourra utiliser comme il le souhaite. En C, ces fonctions sont disponibles dans l'en-tête <stdlib.h>.

L'un des principaux avantages qu'offre le langage C est sa capacité à fournir au programmeur un contrôle poussé sur la gestion de la mémoire. Cette liberté nécessite néanmoins une grande rigueur, tant les problèmes pouvant survenir sont nombreux et souvent difficiles à diagnostiquer. On peut dire sans prendre beaucoup de risque que la plupart des erreurs de programmation en C, ont pour origine une mauvaise utilisation des fonctions de gestion de la mémoire. Il ne faut pas sous-estimer la difficulté de cette tâche. Autant cela est trivial pour un programme de quelques centaines de lignes, autant cela peut devenir un casse-tête quand ledit programme a subi des changements conséquents, pas toujours faits dans les règles de l'art.

La manière dont la mémoire physique d'un ordinateur est conçue, ainsi que la façon dont le système d'exploitation la manipule, sont très variables. Cependant, un modèle assez classique consiste à découper la mémoire en segments, segments dont on garde les références dans des tables de pages : c'est le modèle de segmentation / pagination. Ce modèle offre beaucoup d'avantages par rapport à un accès purement linéaire. Décrire son fonctionnement en détail est hors de la portée de cet ouvrage, mais on pourra noter tout de même :

  • Indépendance totale de l'espace d'adressage entre les processus : un processus ne peut pas accéder à la mémoire d'un autre processus. C'est pourquoi transmettre la valeur d'un pointeur à un autre processus ne servira en général à rien, car le second processus ne pourra jamais accéder à l'emplacement pointé.
  • Gestion fine de la mémoire : les segments sont accédés via plusieurs niveaux d'indirection dans les tables de pages. Cela permet de mettre virtuellement les segments n'importe où dans la mémoire ou même sur un système de fichier. Dans la pratique, éparpiller trop les segments (fragmenter) réduit significativement les performances.

Au niveau des inconvénients, on citera essentiellement un problème de performances. Plusieurs niveaux d'indirection impliquent de multiple lectures en mémoire, extrêmement pénalisant en terme de temps d'exécution, au point où des caches sont nécessaires pour garantir des vitesses acceptables. Même si RAM veut dire mémoire à accès aléatoire, il faut bien garder à l'esprit qu'un accès purement séquentiel (adresse mémoire croissante) peut être jusqu'à cent fois plus rapide qu'une méthode d'accès qui met sans cesse à défaut ce système de cache.

Un processus peut donc demander au système de réserver pour son usage exclusif un secteur mémoire de taille déterminée. Il peut également demander au système de modifier la taille d'une zone précédemment réservée ou de la libérer s'il n'en a plus l'utilité.

Fonctions fournies par <stdlib.h>

malloc: allocation de mémoire

La fonction la plus simple d'allocation mémoire est malloc :

#include <stdlib.h>

void * malloc(size_t taille);

Elle prend en argument la taille que l'on veut allouer et renvoie un pointeur vers une zone mémoire allouée ou un pointeur nul si la demande échoue (la cause d'erreur la plus courante étant qu'il n'y a plus assez d'espace mémoire disponible). Trois remarques peuvent être faites :

  1. malloc renvoie une valeur de type void *, il n'est pas nécessaire de faire une conversion explicite (cela est nécessaire en C++) ;
  2. l'argument taille est de type size_t, l'appel à la fonction malloc devrait toujours se faire conjointement à un appel à l'opérateur sizeof.
  3. La zone mémoire allouée, si l'appel réussit, n'est pas initialisée. C'est une erreur de conception grave que d'accéder au bloc mémoire en s'attendant à trouver une certaine valeur (0 par exemple).

Par exemple, si on souhaite réserver une zone mémoire pour y allouer un entier :

/* Déclaration et initialisation */
int *ptr = NULL;

/* Allocation */
ptr = malloc(sizeof(int));

/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
{
    /* Stockage de la valeur "10" dans la zone mémoire pointée par ptr */
    *ptr = 10;
}
else
{
    /* décider du traitement en cas d'erreur */
}

Si on souhaite allouer de l'espace pour une structure de données plus complexe :

typedef struct{
    double b;
    int a;
    MaStructure *suivant;
} MaStructure;

/* Déclaration et initialisation */
MaStructure *ptr = NULL;

/* Allocation */
ptr = (MaStructure *)malloc(sizeof(MaStructure));

if (ptr != NULL)
{
    /* Initialisation de la structure nouvellement créée */
    ptr->a = 10;
    ptr->b = 3.1415;
    ptr->suivant = NULL;
}

free: libération de mémoire

Le C ne possède pas de mécanisme de ramasse-miettes, la mémoire allouée dynamiquement par un programme doit donc être explicitement libérée. La fonction free permet de faire cette libération.

void free( void * pointeur );

La fonction prend en argument un pointeur vers une zone mémoire précédemment allouée par un appel à malloc, calloc ou realloc et libère la zone mémoire pointée.

Exemple :

/* Déclaration et initialisation */
int *ptr = NULL;

/* Allocation */
ptr = malloc(sizeof(int));

/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
{
    /* ... utilisation de la zone allouée ... */

    /* Libérer la mémoire utilisée */
    free(ptr);
    ptr = NULL; /* Pour éviter les erreurs */
}
else
{
    /* décider du traitement en cas d'erreur */
}

L'utilisation du pointeur après libération de la zone allouée (ou la double libération d'une même zone mémoire) est une erreur courante qui provoque des résultats imprévisibles. Il est donc conseillé :

  • d'attribuer la valeur nulle (NULL) au pointeur juste après la libération de la zone pointée, et à toute autre pointeur faisant référence à la même adresse,
  • de tester la valeur nulle avant toute utilisation d'un pointeur.

De plus, donner à free l'adresse d'un objet qui n'a pas été alloué par une des fonctions d'allocation cause un comportement indéfini.

calloc : allocation avec initialisation à 0

La fonction calloc permet d'allouer une zone mémoire dont tous les bits seront initialisés à 0. Son prototype est légèrement différent de celui de malloc, et est pratique pour l'allocation dynamique de tableaux.

Syntaxe :

void * calloc(size_t nb_element, size_t taille);

De manière similaire à malloc, calloc retourne un pointeur de type void* pointant une zone de nb_element*taille octets allouée en mémoire, dont tous les bits seront initialisés à 0, ou retourne un pointeur nul en cas d'échec.

Exemple :

/* allouer un tableau de 5 entiers */
int* ptr = calloc ( 5, sizeof(int) );

Le pointeur contient l'adresse du premier élément du tableau :

*ptr = 3;   /* premier entier : 3  */

Le pointeur peut être utilisé comme un tableau classique pour accéder aux éléments qu'il contient :

ptr[0] = 3; /* équivaut à  *ptr = 3;      */
ptr[1] = 1; /* équivaut à  *(ptr+1) = 1;  */
ptr[2] = 4; /* équivaut à  *(ptr+2) = 4;  */

Notez que calloc place tous les bits à zéro, mais que ce n'est pas nécessairement une représentation valide pour un pointeur nul ni pour le nombre zéro en représentation flottante. Ainsi, pour initialiser à zéro un tableau de double de manière portable, par exemple, il est nécessaire d'assigner la valeur 0.0 à chaque élément du tableau. Étant donné qu'on initialise chaque élément « manuellement », on peut dans ce cas utiliser malloc plutôt que calloc (la première étant normalement beaucoup plus rapide que la seconde).

realloc

Il arrive fréquemment qu'un bloc alloué n'ait pas la taille suffisante pour accueillir de nouvelles données. La fonction realloc est utilisée pour changer (agrandir ou réduire) la taille d'une zone allouée par malloc, calloc, ou realloc.

Syntaxe :

void * realloc(void * ancien_bloc, size_t nouvelle_taille);

realloc tentera de réajuster la taille du bloc pointé par ancien_bloc à la nouvelle taille spécifiée. À noter :

  • si nouvelle_taille vaut zéro, l'appel est équivalent à free(ancien_bloc).
  • si ancien_bloc est nul, l'appel est équivalent à malloc(nouvelle_taille).
  • En cas de succès, realloc alloue un espace mémoire de taille nouvelle_taille, copie le contenu pointé par le paramètre pointeur dans ce nouvel espace (en tronquant éventuellement si la nouvelle taille est inférieure à la précédente), puis libère l'espace pointé et retourne un pointeur vers la nouvelle zone mémoire.
  • En cas d'échec, cette fonction ne libère pas l'espace mémoire actuel, et retourne une adresse nulle.

Notez bien que realloc ne peut que modifier des espaces mémoires qui ont été alloués par malloc, calloc, ou realloc. En effet, autoriser realloc à manipuler des espaces mémoires qui ne sont pas issus des fonctions de la bibliothèque standard pourrait causer des erreurs, ou des incohérences graves de l'état du processus. En particulier, les tableaux, automatiques comme statiques, ne peuvent être passés à realloc, comme illustré par le code suivant :

Avertissement Ce code contient une erreur volontaire !
void f(void)
{
    int tab[10];
    /* ... */
    int *ptr = realloc(tab, 20 * sizeof(int));
    /* ... */
}

Lorsque realloc reçoit la valeur de tab, qui est un pointeur sur le premier élément (i.e. &tab[0]), il ne peut la traiter, et le comportement est indéfini. Sur cet exemple, il est facile de voir l'erreur, mais dans l'exemple suivant, la situation est plus délicate :

#include <stdint.h> /* pour SIZE_MAX */
#include <stdlib.h>

/* 'double' essaye de doubler l'espace mémoire pointé par ptr.
 *
 * En cas de succès, la valeur renvoyée est un pointeur vers le nouvel espace mémoire, et l'ancienne
 * valeur de ptr est invalide.
 * En cas d'échec, l'espace pointé par ptr n'est pas modifié, et la valeur NULL est renvoyée.
 */
void *double(void *ptr, size_t n)
{
    void *tmp = NULL;
    if ((ptr != NULL) && (n != 0) && (n <= SIZE_MAX / 2))
    {
        tmp = realloc(ptr, 2 * n);
    }
    return tmp;
}

La fonction double en elle-même ne comporte pas d'erreur, mais elle peut causer des plantages suivant la valeur de ptr qui lui est passée. Pour éviter des erreurs, il faudrait que la documentation de la fonction précise les contraintes sur la valeur de ptr... et que les programmeurs qui l'utilisent y fassent attention.

On peut aussi noter que, quand realloc réussit, le pointeur renvoyé peut très bien être égal au pointeur initial, ou lui être différent. En particulier, il n'y a aucune garantie que, quand on diminue la taille de la zone mémoire, il le fasse « sur place ». C'est très probable, car c'est ce qui est le plus facile et rapide à faire du point de vue de l'implémentation, mais rien ne l'empêche par exemple de chercher un autre espace mémoire disponible qui aurait exactement la taille voulue, au lieu de garder la zone mémoire initiale.

On peut noter le test (n <= SIZE_MAX / 2). Il permet d'éviter un débordement entier : si n était supérieur à cette valeur, le produit n * 2 devrait avoir une valeur supérieure à SIZE_MAX, qui est la plus grande valeur représentable par le type size_t. Lorsque cette valeur est passée à realloc, la conversion en size_t, qui est un type entier non signé, se fera modulo SIZE_MAX + 1, et donc la fonction recevra une valeur différente de celle voulue. Si le test n'était pas fait, double pourrait ainsi retourner à l'appelant une zone mémoire de taille inférieure à celle demandée, ce qui causerait des bogues. Ce genre de bogue (non spécifique à realloc) est très difficile à détecter, car n'apparaît que lorsque l'on atteint des valeurs limites, ce qui est assez rare, et le problème peut n'être visible que bien longtemps après que l'erreur de calcul soit faite.

Gestion d'erreur

Pour gérer correctement le cas d'échec, on ne peut faire ainsi :

Avertissement Ce code contient une erreur volontaire !
int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
{
    /* ... */

    /* On se rend compte qu'on a besoin d'un peu plus de place. */
    ptr = realloc(ptr, 20 * sizeof(int));
    /* ... */
}

En effet, si realloc échoue, la valeur de ptr est alors nulle, et on aura perdu la référence vers l'espace de taille 10 * sizeof(int) qu'on a déjà alloué. Ce type d'erreur s'appelle une fuite mémoire. Il faut donc faire ainsi :

int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
{
    /* ... */

    /* On se rend compte qu'on a besoin d'un peu plus de place. */
    int *tmp = realloc(ptr, 20 * sizeof(int));
    if (tmp == NULL)
    {
        /* Exemple de traitement d'erreur minimaliste */
        free(ptr);
        return EXIT_FAILURE;
    }
    else
    {
        ptr = tmp;
        /* On continue */

    }
}

Exemple

realloc peut être utilisé quand on souhaite boucler sur une entrée dont la longueur peut être indéfinie, et qu'on veut gérer la mémoire assez finement. Dans l'exemple suivant, on suppose définie une fonction lire_entree qui :

  • lit sur une entrée quelconque (par exemple stdin) un entier, et renvoie 1 si la lecture s'est bien passée ;
  • renvoie 0 si aucune valeur n'est présente sur l'entrée, ou en cas d'erreur.

Cette fonction est utilisée pour construire un tableau d'entiers issus de cette entrée. Comme on ne sait à l'avance combien d'éléments on va recevoir, on augmente la taille d'un tableau au fur et à mesure avec realloc.

/* Fonction qui utilise 'lire_entree' pour lire un nombre indéterminé
 * d'entiers.
 * Renvoie l'adresse d'un tableau d'entiers (NULL si aucun entier n'est lu).
 * 'taille' est placé au nombre d'éléments lus (éventuellement 0).
 * 'erreur' est placée à une valeur non nulle en cas d'erreur, à une valeur nulle sinon.
 */
int *traiter_entree(size_t *taille, int *erreur)
{
    size_t max = 0;   /* Nombre d'éléments utilisables */
    size_t i = 0;     /* Nombre d'éléments utilisés */
    int *ptr = NULL;  /* Pointeur vers le tableau dynamique */
    int valeur;       /* La valeur lue depuis l'entrée */
    *erreur = 0;

    while (lire_entree(&valeur) != 0)
    {
        if (i >= max)
        {
            /* Il n'y a plus de place pour stocker 'valeur' dans 'ptr[i]' */
            max = max + 10;
            int *tmp = realloc(ptr, max * sizeof(int));
            if (tmp == NULL)
            {
                /* realloc a échoué : on sort de la boucle */
                *erreur = 1;
                break;
            }
            else
            {
                ptr = tmp;
            }
        }
        ptr[i] = valeur;
        i++;
    }
    *taille = i;
    return ptr;
}

Ici, on utilise max pour se souvenir du nombre d'éléments que contient la zone de mémoire allouée, et i pour le nombre d'éléments effectivement utilisés. Quand on a pu lire un entier depuis l'entrée, et que i vaut max, on sait qu'il n'y a plus de place disponible et qu'il faut augmenter la taille de la zone de mémoire. Ici, on incrémente la taille max de 10 à chaque fois, mais il est aussi possible de la multiplier par 2, ou d'utiliser toute autre formule. On utilise par ailleurs le fait que, quand le pointeur envoyé à realloc est nul, la fonction se comporte comme malloc.

Le choix de la formule de calcul de max à utiliser chaque fois que le tableau est rempli résulte d'un compromis :

  • augmenter max peu à peu permet de ne pas gaspiller trop de mémoire, mais on appellera realloc très souvent.
  • augmenter très vite max génère relativement peu d'appels à realloc, mais une grande partie de la zone mémoire peut être perdue.

Une allocation mémoire est une opération qui peut être coûteuse en terme de temps, et un grand nombre d'allocations mémoire peut fractionner l'espace mémoire disponible, ce qui alourdit la tâche de l'allocateur de mémoire, et au final peut causer des pertes de performance de l'application. Aucune formule n'est universelle, chaque situation doit être étudiée en fonction de différents paramètres (système d'exploitation, capacité mémoire, vitesse du matériel, taille habituelle/maximale de l'entrée...). Toutefois, l'essentiel est bien souvent d'avoir un algorithme qui marche, l'optimisation étant une question secondaire. Dans une telle situation, utilisez d'abord une méthode simple (incrémentation ou multiplication par une constante), et n'en changez que si le comportement du programme devient gênant.

Problèmes et erreurs classiques

Défaut d'initialisation d'un pointeur

Pour éviter des erreurs, un pointeur devrait toujours être initialisé lors de sa déclaration ; soit à NULL, soit avec l'adresse d'un objet, soit avec la valeur de retour d'une fonction « sûre » comme malloc. Méditons sur l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
/* Déclaration sans initialisation */
int *ptr;
int var;

/* On stocke dans var la valeur de la zone mémoire pointée par ptr*/
var = *ptr;

Ce code va compiler! Si on est chanceux l'exécution de ce code provoquera une erreur à la troisième étape lorsqu'on déréférence ptr. Si on l'est moins, ce code s'exécute sans souci et stocke dans var une valeur aléatoire, ce qui pourrait provoquer une erreur lors d'une utilisation ultérieure de la variable var alors que l'erreur réelle se situe bien plus en amont dans le code! Une variable (automatique) de type pointeur est en effet comme toutes les autres variables : si elle n'est pas initialisée explicitement, son contenu est indéfini ; son déréférencement peut donc causer n'importe quel comportement.

Lorsqu'on déclare une variable ou qu'on alloue une zone mémoire, on ne dispose d'aucune garantie sur le contenu de cette zone ou de cette variable. Initialiser systématiquement les variables dès leur déclaration, en particulier lorsqu'il s'agit de pointeurs, fait partie des bons réflexes à prendre et pour la plupart des applications ne dégrade pas le temps d'exécution de manière significative. Ce type d'erreur peut être extrèmement difficile à localiser à l'intérieur d'un programme plus complexe.

Référence multiple à une même zone mémoire

La recopie d'un pointeur dans un autre n'alloue pas une nouvelle zone. La zone est alors référencée par deux pointeurs. Un problème peut survenir si on libère la zone allouée sans réinitialiser tous les pointeurs correspondants :

Avertissement Ce code contient une erreur volontaire !
int* ptr = malloc(sizeof(int)); // allouer une zone mémoire pour un entier
int* ptr2 = ptr; // ptr2 pointe la même zone mémoire

*ptr = 5;
printf("%d\n", *ptr2 ); // affiche 5

/* libération */
free( ptr );
ptr = NULL;

*ptr2 = 10; /* <- résultat imprévisible (plantage de l'application, ...) */

En règle générale, il faut éviter que plusieurs variables pointent la même zone mémoire allouée.

L'utilisation d'un outil de vérification statique permet de détecter ce genre d'erreur.

Fuite mémoire

La perte du pointeur associé à un secteur mémoire rend impossible la libération du secteur à l'aide de free. On qualifie cette situation de fuite mémoire, car des secteurs demeurent réservés sans avoir été désalloués. Cette situation perdure jusqu'à la fin du programme principal. Voyons l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
int i = 0;       // un compteur
int* ptr = NULL; // un pointeur
while (i < 1001) {
    ptr = malloc(sizeof(int)); // on écrase la valeur précédente de ptr par une nouvelle
    if (ptr != NULL)
    {
       /* traitement ....*/
    }
}

À la sortie de cette boucle on a alloué un millier d'entiers soit environ 2000 octets, que l'on ne peut pas libérer car le pointeur ptr est écrasé à chaque itération (sauf la dernière). La mémoire disponible est donc peu à peu grignotée jusqu'au dépassement de sa capacité. Ce genre d'erreurs est donc à proscrire pour des processus tournant en boucle pendant des heures, voire indéfiniment. Si son caractère cumulatif la rend difficile à détecter, il existe de nombreux utilitaires destinés à traquer la moindre fuite.

Gestion des signaux

Ébauche

Cette page est considérée comme une ébauche à compléter. Si vous possédez quelques connaissances sur le sujet, vous pouvez les partager en éditant dès à présent cette page (en cliquant sur le lien « modifier »).

Ressources suggérées : Aucune (vous pouvez indiquer les ressources que vous suggérez qui pourraient aider d'autres personnes à compléter cette page dans le paramètre « ressources » du modèle? engendrant ce cadre)

Les signaux permettent une communication, assez sommaire, entre le système et un processus, ou entre différents processus. Cette communication est sommaire, car un signal ne porte qu'une seule information: son numéro, de type int. Un processus peut aussi s'envoyer un signal à lui-même.

Ces signaux sont envoyés de manière asynchrone: lorsqu'un processus reçoit un signal, son exécution est interrompue, et une fonction spécifique, dite gestionnaire de signal, est appelée, avec en paramètre le numéro du signal reçu, pour traiter l'événement. Lorsque cette fonction se termine, le processus reprend là où il s'était arrêté.

C'est à chaque processus de déterminer ce qu'il fait quand il reçoit un signal de numéro donné, en définissant un gestionnaire de signal pour tous les signaux qu'il le souhaite. Vous l'aurez remarqué, dans tous les exemples de ce livre rencontrés jusqu'à présent, nous ne nous sommes pas occupés de savoir quels signaux nous pourrions recevoir, et comment il fallait les traiter. En l'absence de précisions dans nos programmes, l'implémentation fournira en effet un gestionnaire de signal par défaut, qui le plus souvent se contentera d'arrêter le programme.

Les fonctions suivantes, fournies par l'en-tête <signal.h>, sont utilisées dans la gestion des signaux :

  • signal() pour définir un gestionnaire de signal;
  • et raise() pour envoyer un signal au processus courant.

Le C définit ces deux fonctions, et pas plus, alors que les signaux peuvent faire beaucoup plus... En particulier, aucune fonction pour envoyer un signal à un autre processus n'est définie. Cela est dû au fait que, sur certains systèmes, la notion de processus n'existe pas, et une seule tâche s'exécute. Dans de telles situations, il serait aberrant de demander à un compilateur C pour un tel système de fournir des moyens de communications inter-processus ! Par contre, les communications inter-processus étant chose courante dans de nombreux autres systèmes, des extensions fournissent des moyens de le faire (voir par exemple la fonction kill() définie dans POSIX, cf. le chapitre Gestion des signaux du livre Programmation POSIX).

Définir un gestionnaire de signal

La fonction

void (*signal(int sig, void (*func)(int)))(int);

permet de définir un gestionnaire de signal. La signature de cette fonction étant un peu complexe, on peut la simplifier en utilisant un typedef:

typedef void (*t_handler)(int);

t_handler signal(int sig, t_handler func);

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

Ainsi, on voit mieux comment fonctionne cette fonction (notez que le type t_handler n'est pas défini par la norme, et n'est ici que pour clarifier la syntaxe). Elle prend deux paramètres:

  • le numéro du type de signal à traiter;
  • et un pointeur vers la fonction de gestion du signal.

Le premier paramètre est le numéro du signal. La norme définit un faible nombre de signaux, par des macros dans l'en-tête <signal.h>, par exemple SIGSEGV qui indique un accès illégal à la mémoire (SEGmentation Violation), et laisse à l'implémentation la liberté de définir d'autres types de signaux.

Pour le deuxième paramètre, on peut donner trois valeurs possibles:

  • SIG_IGN (macro définie dans <signal.h>): tout signal ayant le numéro sig sera ignoré (i.e. rien ne se passera, le programme continue de s'exécuter);
  • SIG_DFL (id.): le gestionnaire de signal par défaut sera mis en place pour ce type de signal.
  • un pointeur vers une fonction de type t_handler: cette fonction sera appelée lorsqu'un signal de type sig sera reçu par le processus.

Cette fonction renvoie la valeur du dernier gestionnaire de signal pour le numéro donné (qui peut être SIG_IGN ou SIG_DFL).

En cas d'erreur, la fonction renvoie SIG_ERR et place la variable errno à une valeur significative.

Les gestionnaires de signaux

Les gestionnaires de signaux sont des fonctions au prototype simple :

void fonction(int);

Elles prennent un argument de type int, qui est le numéro du signal, et ne renvoient rien. En effet, ces fonctions étant appelées de manière asynchrone, leur éventuelle valeur de retour ne serait récupérée, et encore utilisée, par personne...

Comme on utilise signal pour associer à chaque numéro de signal une fonction qui va gérer les signaux de ce numéro, on pourrait penser qu'il n'est pas nécessaire de donner ce numéro en paramètre à la fonction. Cependant cela s'avère pratique, car on peut ainsi définir une seule fonction qui peut gérer plusieurs types de signaux, et dont le comportement variera en fonction du signal à traiter.

Voici un exemple simple de définition et de mise en place d'un gestionnaire de signaux, pour le type de signal SIG_FPE, qui concerne des exceptions lors des calculs en virgule flottante (Floating Point Exception):

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

#include <signal.h>
#include <stdio.h>

void sig_fpe(int sig)
{
    /* ... */
}

int main(void)
{
    if (signal(SIG_FPE, sig_fpe) == SIG_ERR)
    {
        puts("Le gestionnaire de signal pour SIG_FPE n'a pu etre defini.");
    }
    else
    {
        puts("Le gestionnaire de signal pour SIG_FPE a pu etre defini.");
    }
    /* ... */
    return 0;
}

Conclusion

Le C est un langage plein de paradoxes. Pouvant aussi bien s'accommoder d'applications bas niveau que proposer des interfaces relativement proches des méthodes orientées objets, ce langage a de quoi séduire un large public. Pourtant, force est de constater qu'il a fait souffrir beaucoup de personnes et pas seulement les programmeurs. Issu de l'époque où la mémoire et les capacités de calcul étaient encore des denrées rares et chères, les pionniers de la programmation ont trop souvent privilégié le côté bas niveau de ce langage. Il en résulta des applications difficilement maintenables, abusant des aspects les plus complexes ou non-portables qu'offre le C pour économiser le moindre cycle processeur. C'est de cette époque que sa réputation « d'assembleur plus compliqué et plus lent que l'assembleur » s'est forgée, éclipsant ses aspects de plus haut niveau, qui pourtant méritent une meilleure estime.

Les fonctions de la bibliothèque standard et surtout le langage lui-même contiennent beaucoup de « pièges », notamment par les comportements indéfinis, ou dépendants de l'implémentation, qui rendent des bogues parfois très difficiles à découvrir. Le programmeur doit comprendre que le C lui fournit de grandes possibilités, mais qu'en retour il lui demande une rigueur d'autant plus grande. Apprivoiser ce langage peut nécessiter une longue phase d'apprentissage. Nous espérons que cet ouvrage vous aura permis d'appréhender le C avec plus de sérénité et de casser certains mythes qui ont décidément la vie bien dure.

Il existe beaucoup de bibliothèques qui proposent au programmeur C des outils pour étendre la bibliothèque standard, et permettent ainsi la gestion d'interfaces graphiques, la programmation réseau, le gestion de l'internationalisation, etc. Celles-ci sont très riches, et bien trop nombreuses pour être abordées dans ce livre. Wikilivres propose des livres étudiant certaines de ces bibliothèques. Pour les autres, Internet et ses moteurs de recherche restent de loin les meilleurs outils pour trouver ce dont vous aurez nécessairement besoin.

Le langage C a su s'adapter, d'Unix aux téléphones mobiles, de l'ISO-646 à Unicode. Le C est aujourd'hui un langage encore très utilisé. C'est LE langage de développement de la plupart des micro-contrôleurs actuels. Il a remplacé l'assembleur pour les petites applications. Pour les « grosses » applications tournant sur des OS (Windows, Linux, etc…), il est concurrencé par d'autres langages comme son dérivé le C++, un langage objet encore plus subtil mais permettant tout de même le développement d'applications complexes.

Voir aussi

  • Autotools en C

Notes

  1. ↑ www.freebsd.org/cgi/man.cgi?query=strcasecmp&sektion=3
  2. ↑ 2,0, 2,1 et 2,2 Par exemple, la caractère «é» peut être codé sur deux octets, en utf8, avec la séquence c3 a9 (pour en savoir plus, lire par exemple le wikilivre À la découverte d'Unicode)
  3. ↑ errno peut être définie par une macro ou un identificateur. N'essayez donc pas de récupérer son adresse.

Bibliographie

Vous trouverez ici des références d'ouvrages ou de sites internet ayant servi à l'élaboration de ce wikilivre, ou pouvant être d'intérêt pour une connaissance plus approfondie du C.

Site officiel du WG14 et références normatives

  • Le site officiel du WG14 anglais. Contient de nombreuses informations sur le C, dont les dernières propositions en discussion pour l'évolution de la norme. Attention toutefois, ces documents sont globalement très techniques. On peut noter la présence du Rationale pour la norme C99, qui lui est abordable.
    • Le TC3 n'est pas disponible sur le site du WS, mais sur celui de l'ISO.
  • Rationale de la norme ANSI X3.159-1989 anglais (absent du site du WG14)
  • La page du wiki du groupe de discussion comp.lang.c référençant les documents normatifs et les moyens d'acquérir la norme anglais.

Livres

  • Le « K&R »:
    • Brian W. Kernighan et Dennis M. Ritchie. Le Langage C: Norme ANSI, 2e éd., Dunod, (ISBN 2-100-05116-4) [présentation en ligne]
    • anglais Brian W. Kernighan et Dennis M. Ritchie. The C Programming Language, Second Edition. Prentice Hall, Inc., 1988. (ISBN 0-13-110362-8) [présentation en ligne]
  • Le site Developpez.com propose des critiques de livres sur le C ici (la page contient aussi des livres sur le C++).

Compilateurs C

  • Le wiki de comp.lang.c liste ici un certain nombre de compilateurs en précisant leur support des normes C90 et C99 anglais.

Autres ressources

Tutoriels

  • Les bases du langage C par William Waisse.

Histoire du C

  • The Development of the C Language par Dennis Ritchie anglais.
  • Historique de C par Marc Mongenet.

Licence

Licence de documentation libre GNU

GFDL GFDL Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture.
Récupérée de « http://fr.wikibooks.org/w/index.php?title=Programmation_C/Version_imprimable&oldid=445563 »
Catégories :
  • Versions imprimables
  • Programmation C (livre)
Catégories cachées :
  • À faire
  • Ébauches sans ressources suggérées
  • Wikilivres:ébauches

Menu de navigation

Outils personnels

  • Créer un compte
  • Se connecter

Espaces de noms

  • Page
  • Discussion

Variantes

Affichages

  • Lire
  • Modifier
  • Modifier le wikicode
  • Historique

Plus

Bibliothèque

  • Accueil
  • La vitrine
  • Tous les livres
  • Rechercher un livre
  • Wikijunior

Navigation

  • Modifications récentes
  • Communauté
  • Le Bistro
  • Faire un don
  • Livre au hasard

Aide

  • Aide
  • Wikilivre d'aide

Autres projets

  • Wikipédia
  • Commons
  • Wikiversité
  • Wikisource
  • Wikiquote
  • Wiktionnaire
  • Wikinews
  • Wikivoyage
  • Wikispecies
  • Wikidata
  • Meta

Imprimer / exporter

  • Créer une compilation
  • Télécharger la compilation au format PDF
  • Version imprimable

Outils

  • Pages liées
  • Suivi des pages liées
  • Importer un fichier
  • Pages spéciales
  • Adresse de cette version
  • Information sur la page
  • Citer cette page

Autres langues

  • Polski
Ajouter des liens
  • Dernière modification de cette page le 15 juin 2014 à 01:03.
  • Les textes sont disponibles sous licence Creative Commons attribution partage à l’identique ; d’autres termes peuvent s’appliquer.
    Voyez les termes d’utilisation pour plus de détails.
  • Politique de confidentialité
  • À propos de Wikilivres
  • Avertissements
  • Développeurs
  • Affichage mobile
  • Powered by MediaWiki


Introduction

Historique

Naissance

Le langage C a été inventé aux Bells Labs en 1972 par Dennis Ritchie pour permettre l'écriture du système d'exploitation UNIX, alors développé par Ken Thompson et Dennis Ritchie.

Le système d'exploitation UNIX, né à la fin des années 1960 - début des années 1970, a été écrit directement en langage assembleur pour les machines auxquelles il était destiné. Si le langage assembleur permettait d'écrire un tel système, il n'en était pas moins peu aisé à utiliser. Un tel langage est en effet particulier à un type de processeur, ce qui fait que tout le système devait être réécrit pour le faire fonctionner sur une nouvelle architecture. Cela fait que son principal créateur, Ken Thompson, souhaita rapidement utiliser un langage plus évolué pour réécrire UNIX.

Parmi les langages disponibles à l'époque, BCPL (pour Basic Combined Programming Language, qui est une simplification de CPL), créé par Martin Richards en 1966, était intéressant. Sans entrer dans des descriptions détaillées, BCPL est un langage simple, procédural, et non typé. Sa simplicité permettait de créer facilement des compilateurs BCPL sur les machines de l'époque, où les ressources étaient très limitées (le premier ordinateur utilisé par Ken Thompson pour lancer Unix était un PDP-7, qui disposait d'une mémoire de 4000 mots de 18 bits, soit moins de 9 Ko). Ken Thompson l'a fait évoluer pour concevoir le langage B, qu'il a implémenté sur les premières machines UNIX. Cependant, certaines limitations du langage B ont fait qu'UNIX n'a pu être réécrit dans ce langage.

À partir de 1971, Dennis Ritchie fit évoluer B, pour répondre à ces problèmes. À l'image des programmeurs qui incrémentent les versions de leurs programmes, Ritchie « incrémenta » la lettre B pour appeler le nouveau langage C. Cette évolution se « stabilisa » vers 1973, année à partir de laquelle UNIX et les utilitaires systèmes d'UNIX ont été réécrits avec succès en C.

Développement

Par la suite en 1978, Brian W. Kernighan documenta très activement le langage, pour finalement publier avec Ritchie le livre de référence The C Programming Language. On appelle souvent C K&R le langage tel que spécifié dans la première édition de ce livre.

Dans les années qui suivirent, le langage C fut porté sur de nombreuses autres machines. Ces portages ont souvent été faits, au début, à partir du compilateur pcc de Steve Johnson, mais par la suite des compilateurs originaux furent développés indépendamment. Durant ces années, chaque compilateur C fut écrit en suivant les spécifications du K&R, mais certains ajoutaient des extensions, comme des types de données ou des fonctions supplémentaires, ou interprétaient différemment certaines parties du livre (qui n'était pas forcément très précis). À cause de cela, il fut de moins en moins facile d'écrire des programmes en C qui puissent fonctionner tels quels sur un grand nombre d'architectures.

Normalisation

Pour résoudre ce problème, et ajouter au C des possibilités dont le besoin se faisait sentir (et déjà existantes dans certains compilateurs), en 1989, l'organisme national de normalisation des USA (ANSI) normalisa le C. Le nom exact de cette norme est ANSI X3.159-1989 Programming Language C, mais elle fut (et est toujours) connue sous les dénominations ANSI C ou C89. Puis l'ANSI soumit cette norme à l'Organisation internationale de normalisation (ou ISO), qui l'accepta telle quelle et la publia l'année suivante sous le numéro ISO/IEC 9899:1990 (le document étant connu sous le nom ISO C90, ou C90). Le besoin de normalisation de l'époque étant extrêmement pressant, pratiquement tous les éditeurs de compilateurs C de l'époque se sont mis à modifier leurs compilateurs pour supporter la norme (et même n'ont pas attendu la publication officielle pour lancer ces modifications), et de fait la quasi totalité des implémentations existant à ce jour comprennent ces normes. L'ISO publia dans les années qui suivirent deux ensembles de correctifs, et un amendement pour la gestion des caractères internationaux.

Étant donnée la grande diversité des implémentations existants au moment de l'élaboration de la norme, et le principe de négociation qui base les processus de normalisation, la norme C est un compromis. Son but était principalement double : d'une part assurer le plus possible la portabilité du code C, pour simplifier le portage de programmes C d'une implémentation à une autre (ce qui était souvent un cauchemar à réaliser), d'autre part donner une certaine liberté aux éditeurs pour proposer des extensions spécifiques. Pour ce faire, la norme a défini des niveaux de conformité pour les programmes C et les implémentations, allant du programme strictement conforme, que toute implémentation doit accepter, et qui fonctionnera exactement de la même manière, aux programmes dépendant d'extensions.

On peut noter que le livre de Kernighan et Ritchie a été republié dans une seconde édition, pour refléter les changements du C89.

Enfin, en 1999, l'organisme ISO proposa une nouvelle version de la norme, qui reprenait quelques bonnes idées du langage C++ (voir plus bas). Il ajouta aussi le type long long d'une taille minimale de 64 bits, les types complexes, l'initialisation des structures avec des champs nommés, parmi les modifications les plus visibles. Le nouveau document, qui au niveau de l'ISO est celui ayant autorité aujourd'hui, est ISO/IEC 9899:1999, connu sous le sigle C99. Deux ensembles de correctifs à cette version ont été publiés jusqu'à présent. Au sens strict, la dénomination ISO C correspond donc actuellement à la norme de 1999 corrigée, mais l'usage est tel qu'il est préférable de toujours préciser de quelle version on parle (89/90 ou 99), et c'est ce qui sera fait dans cet ouvrage lorsque la distinction sera nécessaire. Il est à noter que, si la norme C90 a été largement adoptée par les éditeurs de compilateurs C, très peu implémentent la version C99. En effet, le besoin des utilisateurs s'est fait moins pressant, et l'effort nécessaire pour rendre les compilateurs conformes a rarement été mené à terme.

L'activité du groupe de travail de la norme C est, depuis, portée plus sur la « maintenance » du langage qu'à de véritables « évolutions ». La prochaine version, pour l'instant dénommée officieusement C1X et ISO/IEC 9899:201x officiellement, n'est pas encore publiée et ne sera pas étudiée dans ce livre[1].

La norme C spécifie la syntaxe du langage ainsi qu'une bibliothèque de fonctions simple. Cette bibliothèque est moins fournie que dans d'autres langages, mais cela est dû au domaine d'utilisation très varié de ce langage. En particulier, pour assurer une portabilité maximale du langage entre les implémentations embarquées, les ordinateurs de type PC et les supercalculateurs (par exemple), certaines fonctions n'ont pas été acceptées dans la norme. On peut citer, entre autres, la programmation parallèle, les communications entre processus, la communication réseau ou les interfaces graphiques...

Cela fait qu'en parallèle de cette normalisation du langage C, certaines extensions ont elles aussi été standardisées, voire normalisées. Ainsi, par exemple, des fonctions spécifiques aux systèmes UNIX, sur lesquels ce langage est toujours très populaire, et qui n'ont pas été intégrées dans la norme du langage C, ont servi à définir une partie de la norme POSIX.

Dans ce livre, nous étudierons surtout le langage C tel que défini dans les normes ISO citées ci-dessus. La norme C99 n'étant pas implémentée par tous les compilateurs, et beaucoup de programmes existants étant développés en C90, les différences entre les deux normes seront indiquées à chaque fois que nécessaire. Toutefois, nous illustrerons certains chapitres avec des exemples d'extensions courantes qu'un développeur C peut rencontrer, en particulier concernant les fonctions qui ne sont pas fournies par la norme C elle-même. Mais ces exemples d'extensions seront mineurs. D'autres Wikilivres étudient plus en détails de telles extensions, vous pouvez les trouver en regardant les livres listés sur la page Catégorie:C.

Norme et documents de référence

La norme qui définit le langage C est élaborée au niveau international par un groupe de travail (working group) de l'ISO. Ce groupe de travail fait partie plus précisément du comité technique commun à l'ISO et à la Commission Electrotechnique Internationale, le Joint Technical Commitee 1, qui élabore les normes internationales concernant les technologies de l'information. Il se décompose en sous-comités, donc le SC22, responsable des langages informatiques. Le SC22 rassemble plusieurs groupes de travail, chacun portant sur un langage spécifique, ou sur des considérations indépendantes des langages. On peut ainsi citer, par exemple :

  • JTC1/SC22/WG9 pour Ada ;
  • JTC1/SC22/WG14 pour le C ;
  • JTC1/SC22/WG21 pour le C++.

Comme pour toute norme internationale, c'est l'ISO qui décide des modalités de sa distribution. Comme la grande majorité des normes ISO, celle-ci n'est pas en libre distribution, mais en vente[2]. Le lecteur désirant acheter un exemplaire de la norme peut le faire sur le site officiel de l'ISO, par exemple.

Les corrections, ou questions, concernant la norme sont diffusées individuellement par le WG14 sur leur site sous le nom de Defect Report (DR), et rassemblées pour publication officielle dans des rectificatifs techniques ou Technical Corrigenda (TC). Deux ont été publiés pour le C99, en 2001 et 2004.

Par ailleurs, les brouillons (ou drafts) de la norme appartiennent au groupe de travail, qui peut décider de les publier. C'est le cas des documents n869, qui est le dernier brouillon disponible avant la publication de C99, et n1124, correspondant au C99 auquel ont été ajoutés les TC1 et TC2 (le TC3 ayant été publié en 2007).

On pourra noter que, tout comme l'ANSI l'a fait pour le C89, le WG14 a publié un Rationale, qui est un commentaire de la norme C99. Ces deux textes (en anglais, toujours) peuvent être intéressants pour comprendre les motivations de certains choix dans l'évolution du langage.

Tous ces documents sont disponibles sur le site officiel du groupe de travail (les URLs et références se trouvent dans la bibliographie à la fin de ce livre).

Toutefois, il faut bien noter qu'une norme est un texte très technique, qui sert de référence, et est très loin d'être un texte pédagogique. Un débutant fera mieux de lire un ouvrage comme ce wikilivre pour apprendre le C que lire directement la norme, au risque d'être rapidement découragé. Le présent ouvrage est construit de manière à ne pas avoir à recourir au texte de la norme, et n'y fera référence que rarement, mais le lecteur intéressé et averti pourra y vérifier des points complexes qui ne seront pas détaillés ici.

C et C++

Ce livre ne porte pas sur le C++, mais ces deux langages étant d'apparence très proches, il convient, pour éviter la confusion au lecteur désireux d'apprendre l'un ou l'autre, de préciser qu'ils sont très différents et que, si la connaissance de l'un peut aider à l'apprentissage de l'autre, ils ne doivent pas être confondus. L'idée du langage qui est devenu C++ a été lancée par Bjarne Stroustrup en 1979, dans un souci de faire évoluer le C de manière à le rendre plus « robuste ». L'apport le plus visible de cette évolution est que le C++ est orienté objet, alors que le C est procédural, mais ce n'est pas le seul.

Le langage C++ s'est créé à partir du C puis, rapidement, en parallèle à lui, et en 1998 est devenu une norme ISO au même titre que le C. Les groupes de travail respectifs du C et du C++ communiquent régulièrement dans le but d'assurer une certaine compatibilité entre les deux langages. De fait, un certain nombre d'idées issues des réflexions autour du C++ se sont vues intégrées dans la norme C99[3]. Cependant, la croyance populaire selon laquelle le C est un sous-ensemble du C++, ou qu'utiliser un compilateur C++ pour un programme C ne pose pas de problème, est fausse, et à plus d'un titre :

  • un code source strictement conforme du point de vue du C peut être invalide au sens du C++[4] ;
  • un code source qui est à la fois strictement conforme pour le C et le C++ peut avoir un comportement différent dans ces deux langages[5].

Durant l'apprentissage du C, le lecteur devra donc faire attention. Un certain nombre d'éditeurs, mettant à profit la grande proximité des deux langages, distribuent ensemble un compilateur C et un compilateur C++, en appelant parfois l'ensemble C/C++, ce qui entraîne une grande confusion chez les utilisateurs, qui ont parfois du mal à savoir quel est le langage dans lequel ils travaillent. L'utilisateur doit donc apprendre comment fonctionne son implémentation, pour déterminer quel(s) langage(s), norme(s) et extension(s) elle supporte, et comment le faire fonctionner dans l'un ou l'autre mode.

Une liste de compilateurs, avec les supports des normes, est référencée en Bibliographie.

Pourquoi apprendre le langage C ?

Avec son approche bas-niveau, le C permet d'obtenir des programmes très optimisés, pratiquement autant que s'ils avaient été écrits directement en assembleur. Avec un peu d'effort, il est même possible d'utiliser une approche orientée objet, au prix d'une certaine rigueur que le langage et les compilateurs, dans une certaine mesure, sont très loin d'imposer.

Les systèmes d'exploitation pour ordinateur de bureau les plus répandus actuellement sont Windows de Microsoft, Mac OS X d'Apple, et GNU/Linux. Ils sont tous trois écrits en langage C. Pourquoi ? Parce que les systèmes d'exploitation tournent directement au dessus du matériel de la machine. Il n'y a pas de couche plus basse pour gérer leurs requêtes. À l'origine, les systèmes d'exploitation étaient écrits en assembleur, ce qui les rendait rapides et performants. Toutefois, écrire un OS en assembleur est une tâche pénible et cela produit du code qui ne peut s'exécuter que sur une seule architecture processeur, comme l'Intel X86 ou l'AMD 64. Écrire un OS dans un langage de plus haut niveau, comme le langage C, permet au programmeur de porter son système d'exploitation sur une autre architecture sans avoir à tout réécrire.

Kenneth Thompson (à gauche) et Dennis Ritchie (à droite), les créateurs du langage C

Mais pourquoi utiliser le C et non Java, Basic ou Perl ? Principalement à cause de la gestion de la mémoire. À la différence de la plupart des autres langages de programmation, le langage C permet au programmeur de gérer la mémoire de la manière qu'il aurait choisie s'il avait utilisé l'assembleur. Les langages comme le Java et le Perl permettent au programmeur de ne pas avoir à se soucier de l'allocation de la mémoire et des pointeurs. C'est en général un point positif, car il est assez pénible et inutile de devoir se soucier de la gestion de la mémoire lorsqu'on écrit un programme de haut niveau comme un rapport sur les résultats trimestriels.

Cependant lorsqu'on parle d'écrire un programme de bas niveau comme la partie du système d'exploitation qui s'occupe d'envoyer la chaîne d'octets correspondant à notre rapport trimestriel depuis la mémoire de l'ordinateur vers le buffer de la carte réseau afin de l'envoyer vers une imprimante réseau, avoir un accès direct à la mémoire est fondamental — ce qui est impossible à faire dans un langage comme Java par exemple. Les compilateurs C produisent de plus très souvent un code rapide et performant.

Est-ce vraiment si merveilleux que le langage C soit un langage si répandu ? Par effet domino, la génération suivante de programmes suit la tendance de ses ancêtres. Les systèmes d'exploitation écrits en C ont toujours des librairies écrites en C. Ces bibliothèques système sont à leur tour utilisées pour écrire des bibliothèques de plus haut niveau (comme OpenGL ou GTK) et le programmeur de ces bibliothèques décide souvent d'utiliser le même langage que celui utilisé par ces bibliothèques système. Les développeurs d'applications utilisent ces bibliothèques de haut niveau pour écrire des traitements de texte, des jeux, les lecteurs multimédia, etc... La plupart d'entre eux choisiront d'utiliser pour leur programme le même langage que les bibliothèques de haut niveau. Et le schéma se reproduit à l'infini...


Bases du langage

Bonjour !

L'un des plus petits programmes possible en langage C est :

  1. #include <stdio.h>

  2.

  3. int main(void)

  4. {

  5.     printf("Bonjour !\n");

  6.     return 0;

  7. }

(Les numéros de lignes ont été ajoutés pour la lisibilité, mais ne font pas partie du programme.)

Ce programme a pour seul but d'afficher le texte « Bonjour ! » suivi d'un retour à la ligne. Voici la description de chaque ligne qui le compose :

  1. #include <stdio.h>

Inclusion de l'en-tête nommé <stdio.h>. Il est lié à la gestion des entrées et sorties (STanDard Input Output, entrées/sorties standards) et contient, entre autres, la déclaration de la fonction printf permettant d'afficher du texte formaté.

  3. int main(void)

Définition de la fonction principale du programme, main (principal en anglais). Il s'agit de la fonction appelée au démarrage du programme, tout programme en C doit donc la définir. La partie entre parenthèses spécifie les paramètres que reçoit le programme : ici, le programme n'utilisera aucun paramètre, ce qui est spécifié en C avec le mot-clé void (vide). La partie à gauche du nom de la fonction main spécifie le type renvoyé par la fonction : la fonction main est définie par la norme C comme renvoyant une valeur entière, de type int (pour integer, entier), au système d'exploitation.

  4. {

Début de la définition de la fonction main.

  5.     printf("Bonjour !\n");

Appel de la fonction printf (print formatted, affichage formaté, qui sera expliquée dans le chapitre dédié à cette fonction). La chaîne « Bonjour ! » va être affichée, elle sera suivie d'un retour à la ligne, représenté en C par \n.

  6.     return 0;

La fonction main a été définie comme retournant (to return) une valeur de type int, on renvoie donc une telle valeur. Par convention, la valeur 0 indique au système d'exploitation que le programme s'est terminé normalement.

  7. }

Fin de la définition de la fonction main.

Ce premier exemple ne détaille évidemment pas tout ce qui concerne la fonction main, ou printf, par exemple. La suite de l'ouvrage précisera les points nécessaires dans les chapitres appropriés (voir par exemple le paragraphe la fonction main du chapitre Fonctions et procédures, ou le paragraphe Sorties formatées du chapitre Entrées/sorties).

Compilation

Pour exécuter un programme C, il faut préalablement le compiler?.

Tapez le code source du programme Bonjour ! dans un éditeur de texte? et sauvegardez-le sous le nom bonjour.c (l'extension .c n'est pas obligatoire, mais est usuelle pour un fichier source en C). Ouvrez une fenêtre de commandes (terminal sous Unix, commandes MS-DOS sous Windows), et placez-vous dans le répertoire où est sauvegardé votre fichier. Pour compiler avec le compilateur standard de votre système, il faut taper la commande :

cc bonjour.c

D'autres compilateurs pour le langage C peuvent être utilisés. La compilation du programme produira un fichier exécutable, appelé a.out sous Unix, et a.exe sous Windows. Il peut être exécuté en tapant ./a.out sous Unix, et a sous Windows.

Éléments de syntaxe

Identificateurs

Les identificateurs commencent par une lettre ou le caractère souligné ("_") et peuvent contenir des lettres, des chiffres et le caractère souligné (cependant, les identificateurs commençant par deux caractères soulignés, ou un caractère souligné suivi d'une majuscule sont réservés par l'implémentation, et ne doivent pas être utilisés dans un programme « ordinaire »). Tous les mots-clés ainsi que les symboles (variables, fonctions, champs, etc.) sont sensibles à la casse des lettres. Quelques exemples d'identificateurs valides :

toto
_coin_coin76

Mots réservés du langage

Le langage, dans la norme C90, possède 32 mots réservés (ou mots-clés) qui ne peuvent pas être utilisés comme identificateurs. La norme C99 en a ajouté cinq ; la norme C11 en a de nouveau apporté 7. Le tableau ci-dessous les liste tous, avec la mention « C99 » ou « C11 » pour indiquer ceux définis uniquement dans les versions correspondantes ou ultérieures :

auto | break | case | char | const | continue | default | do
double | else | enum | extern | float | for | goto | if
inline (C99) | int | long | register | restrict (C99) | return | short | signed
sizeof | static | struct | switch | typedef | union | unsigned | void
volatile | while | _Alignas (C11) | _Alignof (C11) | _Atomic (C11) | _Bool (C99) | _Complex (C99) | _Generic (C11)
_Imaginary (C99) | _Noreturn (C11) | _Static_assert (C11) | _Thread_local (C11)

Un programme C90 peut par exemple définir inline comme un identifiant de variable ou de fonction mais cela est très déconseillé, car il sera invalide au sens de la norme C99. Une mise à jour du compilateur peut alors l'empêcher de compiler le code source en question. Pour éviter de telles situations, il est préférable de considérer tous les mots-clés du tableau précédent comme étant réservés, quels que soient le compilateur et la norme utilisés.

Commentaires

Les commentaires commencent par /* et se terminent par */, ils ne peuvent pas être imbriquées :

/* ceci est un commentaire */

/*

 ceci est aussi un commentaire

 */

/* /* ceci est encore un commentaire */
Avertissement Ce code contient une erreur volontaire !
/* /* */ ceci n’est pas un commentaire */

Inclure des commentaires pertinents dans un programme est un art subtil. Cela nécessite un peu de pratique pour savoir guider les lecteurs et attirer leur attention sur certaines parties délicates du code. Pour faire court, on ne saurait trop que rappeler ce célèbre vers de Nicolas Boileau, qui disait que « ce que l'on conçoit bien, s'énonce clairement et les mots pour le dire arrivent aisément » (Art Poétique, 1674). Ou encore, pour adopter une approche plus pragmatique : si un programmeur a du mal à commenter (expliquer, spécifier) le fonctionnement d'un passage de code, c'est que le code est mal conçu et qu'il serait bénéfique de le réécrire plus lisiblement.

Attention! Les commentaires ne devraient pas être employés pour désactiver certaines parties du code. Cette technique dissuade un programmeur d'en inclure puisque le langage C ne permet pas d'imbriquer les commentaires. Pour cela, le préprocesseur dispose d'instructions dédiées, qui permettent heureusement de désactiver du code en laissant les commentaires.

Le // ajoute la possibilité de placer un commentaire d'une seule ligne, à partir du // jusqu'à la fin de la ligne :

// commentaire
instruction; // autre commentaire

Cependant, ces commentaires « à la C++ » sont apparus dans C99, et ne sont pas permis en C90.

Instructions

Les instructions se terminent par un point-virgule (;), on peut placer autant d'instructions que l'on veut sur une même ligne (même si ce n'est pas conseillé pour la lisibilité du code). Les blocs d'instructions commencent par une accolade ouvrante ({) et se terminent par une accolade fermante (}). Les instructions doivent obligatoirement être déclarées dans une fonction : il est impossible d'appeler une fonction pour initialiser une variable globale par exemple (contrairement au C++).

/* une instruction */
i = 1;

/* plusieurs instructions sur la même ligne */
i = 1; j = 2; printf("bonjour\n");

/* un bloc */
{
        int i;
        i = 5;
}

/* l'instruction vide */
;

Dans la mesure où le compilateur ne se soucie pas des blancs (espaces et retours à la ligne), vous pouvez formater votre code comme vous l'entendez. Il y a beaucoup de religions concernant les styles d'indentation, mais pour faire court et éviter les guerres saintes, on ne saurait trop conseiller que d'utiliser le même nombre de blancs par niveau d'imbrication de bloc.

À noter que l'instruction vide étant valide en C, on peut donc pratiquement rajouter autant de point-virgules que l'on veut. Le point-virgule ne sert pas uniquement à marquer la fin des instructions, les compilateurs l'utilisent généralement comme caractère de synchronisation, suite à une erreur dans un programme source. En fait, en général, lorsqu'une erreur est détectée, les compilateurs ignorent tout jusqu'au prochain point-virgule. Ce qui peut avoir des conséquences assez dramatiques, comme dans l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
int traite_arguments(int nb, char * argv[])
{
         /* ... */
         return 0
}

int main(int nb, char * argv[])
{
         int retour;
         retour = traite_arguments(nb, argv);
         /* ... */
}

On notera l'absence de point-virgule à la fin de l'instruction return à la fin de la fonction traite_arguments. Ce que la plupart des compilateurs feront dans ce cas sera d'ignorer tout jusqu'au prochain point-virgule. On se rend compte du problème : on a sauté une déclaration de fonction (avec ses deux paramètres) et une déclaration de variable. Ce qui veut dire qu'une cascade d'erreurs va suivre suite à l'oubli... d'un seul caractère (;) !

Déclarations de variables

T var1, var2, ..., varN;

Cette ligne déclare les variables var1, var2, ..., varN de type T. Une variable est dite locale, si elle est définie à l'intérieur d'une fonction et globale si définie en-dehors.

Par exemple :

int jour; /* 'jour' est une variable globale, de type entier */

int main(void)
{
    double prix; /* 'prix' est une variable locale à la fonction 'main', de type réel */
    return 0;
}

Variables locales

Les variables locales (aussi appelées automatiques) ne sont visibles que dans le bloc dans lequel elles sont définies, et n'existent que durant ce bloc. Par exemple :

Avertissement Ce code contient une erreur volontaire !
int fonction(int n)
{
    int i; /* i est visible dans toute la fonction */
    i = n + 1;
    {  /* début d'un nouveau bloc */
        int j; /* j est visible dans le nouveau bloc *seulement* */
        j = 2 * i; /* i est accessible ici */
    }
    i = j; /* ERREUR : j n'est plus accessible */
    return i;
}

Le code précédent accède à i depuis un bloc contenu dans le bloc où i a été défini, ce qui est normal. Puis il essaye d'accéder à la valeur de la variable j en-dehors du bloc où elle à été définie, ce qui est une erreur.

Les variables locales ne sont pas initialisées automatiquement et contiennent donc, après leur déclaration, une valeur aléatoire. Utiliser une telle valeur peut causer un comportement aléatoire du programme. Le programme suivant essaye d'afficher la valeur d'une telle variable, et on ne peut savoir à l'avance ce qu'il va afficher. Si on le lance plusieurs fois, il peut afficher plusieurs fois la même valeur aussi bien qu'il peut afficher une valeur différente à chaque exécution :

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>

int main(void)
{
    int n;
    printf("La variable n vaut %d\n", n);
    return 0;
}

Avant la normalisation ISO C99, les déclarations de variables locales devaient obligatoirement être placées juste au début d'un bloc et s'arrêtaient à la première instruction rencontrée. Si une déclaration est faite au-delà, une erreur sera retournée. Suivant la norme C99, les déclarations peuvent se trouver n'importe où dans un bloc. Par exemple, le code suivant est correct suivant la norme C99, mais pas suivant la norme C90, car la variable a est définie après l'instruction puts("Bonjour !"); :

int ma_fonction(int n)
{
    puts("Bonjour !");
    int a;  /* Valide en C99, invalide en C90 */
    /* autre chose... */
    return a;
}

Variables globales

Une attention particulière doit être portée aux variables globales, souvent source de confusion et d'erreur. Utilisez des noms explicites, longs si besoin et limitez leur usage au seul fichier où elles sont déclarées, en les déclarants statiques.

Les variables globales sont initialisées avant que la fonction main s'exécute. Si le programmeur ne fournit pas de valeur initiale explicitement, chaque variable reçoit une valeur par défaut suivant son type :

  • un nombre (entier, réel ou complexe) est initialisé à 0 ;
  • les pointeurs sont initialisés à NULL ;
  • pour une structure, l'initialisation se fait récursivement, chaque membre étant initialisé suivant les mêmes règles ;
  • pour une union, le premier membre est initialisé suivant ces règles.

Pour initialiser explicitement une variable globale, on ne peut utiliser que des constantes ; en particulier, on ne peut appeler de fonction, contrairement à d'autres langages.

Avertissement Ce code contient une erreur volontaire !
int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour = jour_courant(); /* ERREUR ! */

int main(void)
{
   /* ... */
   return 0;
}

Le code précédent ne pourra pas compiler, car on ne peut appeler directement jour_courant() pour initialiser jour. Si vous avez besoin d'un tel comportement, une solution est de faire l’initialisation explicite dans main :

int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour; /* 'jour' est initialisé à 0 *avant* le début de 'main' */

int main(void)
{
   jour = jour_courant();
   /* ... */
   return 0;
}

Traitements des ambiguïtés

Le C utilise un mécanisme élégant pour lever des constructions syntaxiques en apparence ambiguës. L'analyse des mots (lexèmes, token en anglais) se fait systématiquement de la gauche vers la droite. Si plusieurs lexèmes peuvent correspondre à une certaine position, le plus grand aura priorité. Considérez l'expression valide suivante :

a+++b;

Ce genre de construction hautement illisible et absconse est bien évidemment à éviter, mais illustre parfaitement bien ce mécanisme. Dans cet exemple, le premier lexème trouvé est bien sûr l'identificateur 'a' puis le compilateur a le choix entre l'opérateur unaire '++', ou l'opérateur binaire '+'. Le plus grand étant le premier, c'est celui-ci qui aura priorité. L'instruction se décompose donc en :

(a ++) + b;


Types de base

Le C est un langage typé statiquement : chaque variable, chaque constante et chaque expression, a un type défini à la compilation. Le langage lui-même fournit des types permettant de manipuler des nombres (entiers, réels ou complexes) ou des caractères (eux-mêmes étant manipulés comme des entiers spéciaux), et permet de construire des types plus complexes à partir de ces premiers, par exemple en groupant des données de même type en tableaux, ou des données de types différents dans des structures. Dans ce chapitre, nous étudierons les types de base fournis par le C, l'étude des types complexes étant faite dans la suite du livre.

Entiers

Il y a cinq types de variables entières (« integer » en anglais) :

  • char ;
  • short int, ou plus simplement short ;
  • int ;
  • long int, ou long ;
  • long long int, ou long long (ce type a été ajouté depuis la norme C99).

Comme évoqué en introduction, le type caractère char est particulier, et sera étudié en détail plus bas.

Les types entiers peuvent prendre les modificateurs signed et unsigned qui permettent respectivement d'obtenir un type signé ou non signé. Ces modificateurs ne changent pas la taille des types. Le langage ne définit pas exactement leur tailles, mais définit un domaine de valeurs minimal pour chacun.

Représentation des entiers signés

La norme C tient compte des anciennes représentations des nombres signés telles que le signe+valeur absolue et le complément à 1. Ces deux représentations sont brièvement expliquées ci-dessous :

Signe + valeur absolue (SVA)
Un bit contient le signe du nombre (par ex : 0 pour +, 1 pour -), les autres bits sont utilisés pour la valeur absolue. On peut donc représenter 0 de deux manières : +0 (000...000) ou -0 (100..000). Sur N bits on peut donc représenter tout nombre entre -(2N-1-1) (111...111) et +(2N-1-1) (011...111).
Complément à 1 (CPL1)
Les bits des nombres négatifs sont inversés. On peut donc représenter 0 de deux manières : +0 (000...000) ou -0 (111..111). Sur N bits on peut donc représenter tout nombre entre -(2N-1-1) (100...000) et +(2N-1-1) (011...111).

Ces deux représentations peuvent représenter la valeur nulle de deux manières différentes.

La représentation moderne des nombres négatifs utilise le complément à 2 (CPL2) qui consiste à représenter les nombres négatifs comme le complément à 1 et en ajoutant 1. Sur un nombre fixe de bits, la valeur 0 n'a qu'une seule représentation : +0 (000...000) et -0 (111...111 + 1 = (1)000...000) ont deux représentations identiques. Sur N bits on peut donc représenter tout nombre entre -(2N-1) (100...000) et +(2N-1-1) (011...111). Cette représentation possède donc un domaine plus large.

Le tableau ci-dessous donne le domaine des valeurs quelle que soit la représentation utilisée (SVA, CPL1 ou CPL2) :

Domaines de valeurs minimaux des types entiers (C90 et C99) quelle que soit sa représentation (SVA, CPL1 ou CPL2)
Type | Taille | Borne inférieure | Borne inférieure (formule) | Borne supérieure | Borne supérieure (formule)
signed char | ≥ 8 bits | -128 | -(27) | +127 | 27-1
unsigned char | ≥ 8 bits | 0 | 0 | +255 | 28-1
short | ≥ 16 bits | -32 767 | -(215-1) | +32 767 | 215-1
unsigned short | ≥ 16 bits | 0 | 0 | +65 535 | 216-1
int | ≥ 16 bits | -32 767 | -(215-1) | +32 767 | 215-1
unsigned int | ≥ 16 bits | 0 | 0 | +65 535 | 216-1
long | ≥ 32 bits | -2 147 483 647 | -(231-1) | +2 147 483 647 | 231-1
unsigned long | ≥ 32 bits | 0 | 0 | +4 294 967 295 | 232-1
long long (C99) | ≥ 64 bits | -9 223 372 036 854 775 807 | -(263-1) | +9 223 372 036 854 775 807 | 263-1
unsigned long long (C99) | ≥ 64 bits | 0 | 0 | +18 446 744 073 709 551 615 | 264-1

Cette table signifie qu'un programme peut utiliser sans problème une variable de type int pour stocker la valeur 215-1, quel que soit le compilateur ou la machine sur laquelle va tourner le programme.

Par contre, une implémentation C peut fournir des domaines de valeurs plus larges que ceux indiqués au-dessus :

  • Les domaines indiqués pour les nombres signés dans le tableau précédent sont ceux d'une implémentation par complément à 1, ou par signe et valeur absolue. Pour le complément à 2, la borne inférieure est de la forme -2N-1, ce qui autorise une valeur supplémentaire (ex : int de -215 à +215-1, soit de -32 768 à +32 767),
  • un int implémenté sur 32 bits pourrait aller de -(231-1) à 231-1, par exemple[6], et un programme tournant sur une telle implémentation peut alors utiliser ces valeurs, mais il perdrait en portabilité.
Domaines de valeurs des types entiers représentés en complément à 2 (CPL2)
Type | Borne inférieure | Borne inférieure (formule) | Borne supérieure | Borne supérieure (formule)
signed char | -128 | -(27) | +127 | 27-1
unsigned char | 0 | 0 | +255 | 28-1
short | -32 768 | -(215) | +32 767 | 215-1
unsigned short | 0 | 0 | +65 535 | 216-1
int | -32 768 | -(215) | +32 767 | 215-1
unsigned int | 0 | 0 | +65 535 | 216-1
long | -2 147 483 648 | -(231) | +2 147 483 647 | 231-1
unsigned long | 0 | 0 | +4 294 967 295 | 232-1
long long (C99) | -9 223 372 036 854 775 808 | -(263) | +9 223 372 036 854 775 807 | 263-1
unsigned long long (C99) | 0 | 0 | +18 446 744 073 709 551 615 | 264-1

Par ailleurs, une relation d'ordre entre ces domaines de valeurs est garantie ; qui peut être exprimée ainsi :

domaine(char) ≤ domaine(short) ≤ domaine(int) ≤ domaine(long) ≤ domaine(long long)

Cela signifie que toutes les valeurs possibles pour une variable du type char sont aussi utilisables pour les autres types ; mais aussi que, par exemple, une valeur valide pour le type int peut ne pas être représentable dans une variable de type short.

Si vous ne savez pas quel type donner à une variable de type entier, le type int est par défaut le meilleur choix (à condition que votre donnée ne dépasse pas 215-1) : ce type est la plupart du temps représenté au niveau matériel par un « mot machine », c'est-à-dire qu'il est adapté à la taille que la machine peut traiter directement (il fait usuellement 32 bits sur un PC 32 bits, par exemple). Cela permet un traitement plus rapide par le matériel. De plus, beaucoup de bibliothèques (que ce soit celle fournie par le langage C ou d'autres) utilisent ce type pour passer des entiers, ce qui fait que l'utilisation de ces bibliothèques sera plus aisée.

Par ailleurs, un utilisateur peut connaître les domaines de valeurs exacts de sa machine en utilisant l'en-tête <limits.h>.

Portabilité apportée par C99

L'incertitude sur l'intervalle de valeur de chaque type en fonction de la machine peut s'avérer extrêmement gênante, pour ne pas dire rédhibitoire. En effet, certains programmes peuvent nécessiter un type de données de taille fixe et cependant être destinés à être portables. Pour ces programmes, les types entiers du C ne sont pas suffisants. Beaucoup d'extensions ont été rajoutées pour définir explicitement des types entiers à intervalle fixe (8, 16, 32 bits...) à partir des types de base, avec une nomenclature loin d'être homogène d'un compilateur à l'autre (ce qui, loin de résoudre le problème, ne faisait que le déplacer).

La norme ISO C99 décide une bonne fois pour toute de définir, dans l'en-tête <stdint.h>, plusieurs nouveaux types où N représente un nombre entier définissant la taille requise en bit :

  • des types implémentés optionnellement sur certaines architectures (à éviter ?) ;
    • entiers signés ou non et de longueur N exacte : uintN_t et intN_t ;
    • entiers pouvant contenir un pointeur : intptr_t et uintptr_t ;
  • des types requis par toutes les architectures respectant la norme C99 ;
    • entiers devant être plus grand que N bits au moins : int_leastN_t et uint_leastN_t ;
    • entiers rapides à calculer et plus grand que N bits au moins : int_fastN_t et uint_fastN_t ;
    • plus grand entier : intmax_t et uintmax_t.

Cet en-tête définit aussi des constantes pour les valeurs minimales et maximales de chaque type.

L'include <inttypes.h> définit les constantes symboliques à utiliser pour imprimer ces nouveaux types avec les fonctions de la famille de printf (PRIxxx) et les lire avec celles de scanf (SCNxxx).

Constantes numériques entières

Il existe différentes suites de caractères qui sont reconnues comme étant des constantes numériques entières :

  • un nombre en notation décimale : une suite de chiffres (0-9) ;
  • le caractère « 0 » suivi d'un nombre en notation octale : une suite de chiffres compris entre 0 et 7 ;
  • les caractères « 0x » suivi d'un nombre en notation hexadécimale : une suite de chiffres et des lettres a, b, c, d, e, f (ou A, B, C, D, E, F).

Par défaut, une constante numérique entière est de type int et, si sa valeur est trop grande pour le type int, elle prend celle du type « plus grand » suffisant. Comme les domaines de valeurs des types peuvent varier suivant la machine, le type effectif d'une constante peut lui aussi varier. Cela peut s'avérer problématique lors de passage de paramètres à des fonctions à nombre variable d'arguments, par exemple. À cause de cela, il est recommandé de forcer le type de la constante en le postfixant des attributs suivants :

  • U : la constante est non-signée (voir la section promotion pour comprendre les implications) ;
  • L : la constante est de type long au lieu de int ;
  • LL est une nouveauté C99 pour les constantes de type long long.

U peut être combiné à L et LL pour obtenir les types unsigned long et unsigned long long, respectivement. Lorsqu'une constante est suffixée, mais que sa valeur est trop grande pour le type demandé, le même processus de recherche de type « assez grand » est utilisé[7].

Débordement

Sur une machine donnée, un type entier a un domaine de valeurs fixe. Considérons qu'on travaille sur un PC en 32 bits, en complément à deux : sur un tel ordinateur, le type int varie souvent de -231 à 231-1. Cela permet de manipuler sans problème des valeurs dans ce domaine. Par contre, si on utilise des valeurs hors du domaine, par exemple 232, et qu'on essaye de la stocker dans une variable de type int sur une telle machine, que se passe-t-il ?

La réponse dépend du type:

  • Si on essaye d'enregistrer une valeur hors domaine dans une variable de type signé (comme dans l'exemple), la conversion n'est pas définie par le langage. Cela signifie que tout peut arriver.
  • Si on essaye d'enregistrer une valeur hors domaine dans une variable de type non signé (unsigned long, par exemple), la conversion se fait modulo la valeur maximale représentable par ce type + 1.

Exemples : On suppose que le type unsigned char est codé sur 8 bits. Alors une valeur de ce type peut aller de 0 à 28-1, soit 255. Par la règle précédente, les conversions se feront donc modulo 255 + 1, soit 256. On considère le programme suivant :

#include <stdio.h>

int main(void)
{
    unsigned char c = 300;
    /* %hhu sert à dire à printf() qu'on veut afficher un unsigned char */
    printf("La variable c vaut %hhu.\n", c);
    c = -5;
    printf("La variable c vaut %hhu.\n", c);
    return 0;
}

Le résultat du programme est :

La variable c vaut 44.
La variable c vaut 251.

En effet, on a 300 - 256 = 44 et -5 + 256 = 251.

De même :

#include <stdio.h>

int main(void)
{
    signed char c = 300;
    /* %hhd sert à dire à printf() qu'on veut afficher un signed char */
    printf("La variable c vaut %hhd.\n", c);
    c = -300;
    printf("La variable c vaut %hhd.\n", c);
    return 0;
}

Le résultat du programme peut alors être :

La variable c vaut 44.
La variable c vaut -44.

Sur une telle machine, les types signés sont traités de la même manière que les types non signés.

Si vous essayez de compiler ces deux programmes, votre compilateur pourra détecter les débordements et vous en avertir (GCC le fait).

Réels

Les nombres réels ne pouvant tous être représentés, sont approximés par des nombres à virgule flottante. Comme dans le cas des entiers, il existe plusieurs types de nombre à virgule flottante. En voici la liste triée par précision croissante :

  • float ;
  • double ;
  • long double.

La norme C90 était assez floue concernant les nombres à virgule flottante, leurs représentations, la précision des opérations, etc., ce qui fait que c'était un des domaines où la conception de programmes utilisant les nombres flottants était chose peu aisée. Le C99 a clarifié les choses en précisant qu'une implémentation C devait respecter la norme IEC 60559:1989 Arithmétique binaire en virgule flottante pour systèmes à microprocesseur. Cette norme (dérivée de IEEE 754) définit des formats de données pour les nombres à virgule flottante, ainsi que des opérations et fonctions sur ces nombres. Elle garantit, entre autres :

  • que certains types de données auront toujours le même format ;
  • et que les calculs effectués sur un type de donnée donneront toujours le même résultat.

Elle définit de plus comment sont gérés les cas exceptionnels, comme les infinis, les NaN (pour Not a Number, résultant par exemple de la division 0/0), etc.

Les types flottants du C correspondent aux type IEC 60559 de la manière suivante :

  • float : simple précision
  • double : double précision
  • long double : suivant l'implémentation, soit la double précision étendue, soit un type non-IEC 60559 (mais de précision au moins égale à double), soit double précision.

Constantes réelles

Une suite de caractères représente une constante à virgule flottante si :

  • c'est une suite de chiffres séparée par un caractère « point », cette séparation pouvant s'effectuer à n'importe quel endroit de la suite (0.0, .0 et 0. représentent tous les trois la valeur 0 de type double) ;
  • un nombre suivi d'un caractère « e » suivi d'un entier.

Dans le deuxième cas, le nombre peut être soit un entier, soit un réel du premier cas.

Les constantes sont de type double par défaut. Pour demander le type float, il faut la suffixer par f ou F, et pour le type long double par l ou L.

Arithmétique

Une attention particulière doit être portée sur la précision des types réels. Ces différents types ne font qu'approximer l'ensemble des nombres réels, avec une précision finie. Des erreurs d'arrondis sont à prévoir, ce qui est très problématique pour des domaines qui n'en tolèrent pas (notamment pour les applications financières, il est conseillé de ne pas utiliser de calcul en virgule flottante).

Le type float en particulier a une précision minimale, qui est bien souvent insuffisante. Voici un exemple classique d'erreur à ne pas faire qui illustre les problèmes liés à la précision des types flottants :

#include <stdio.h>

int main(void)
{
    float i = 0;
    int j;
    for (j = 0; j < 1000; j ++)
    {
        i += 0.1;
    }
    printf("i = %f\n", i);
    return 0;
}

Le résultat est 99,999046, ce qui montre que la précision du type float est en général mauvaise, d'autant plus que le nombre 0,1 n'est pas représentable de manière exacte en binaire[8]. Il est ainsi conseillé d'utiliser le type double à la place de float autant que possible. Dans ce cas de figure, il est préférable d'éviter les accumulations d'erreurs infinitésimales, en réécrivant le code de la manière suivante :

#include <stdio.h>

int main(void)
{
    float i;
    int j;
    for (j = 0; j < 1000; j++)
    {
        i = j * 0.1;
    }
    printf("i = %f\n", i);
    return 0;
}

C'est probablement plus coûteux, car on effectue un millier de multiplications au lieu d'un millier d'additions, mais cela permet d'avoir une précision nettement meilleure que le code précédent.

Pour plus d'informations sur ce domaine, un wikilivre Arithmétique flottante est disponible.

Caractères

À l'origine, le type permettant de représenter un caractère est char. Même si un char n'est plus toujours suffisant aujourd'hui pour représenter un caractère quelconque.

Ce type est un peu plus particulier que les autres, d'une part parce que sa taille définit l'unité de calcul pour les quantités de mémoire (et donc pour les tailles des autres types du langage) et d'autre part son domaine de valeur peut grandement varier de manière relativement inattendue.

Par définition, la taille du type char, notée sizeof(char), vaut toujours 1. Cependant, il faut faire attention : contrairement à ce qu'on pense souvent, un char au sens du C ne vaut pas toujours un octet. Il occupera au minimum 8 bits, mais il existe des architectures, relativement spécialisées il est vrai, ayant des char de 9 bits, de 16 bits, voire plus. Même si, dans une large majorité des cas, les compilateurs utilisent des char de 8 bits, à la fois par simplicité (les machines modernes fonctionnent généralement en 8, 16, 32 ou 64 bits) et pour éviter des problèmes de portabilité de code (beaucoup de codes C existants reposent sur l'hypothèse que les char font 8 bits, et risqueraient de ne pas marcher sur une autre architecture)[9]. Par simplification, nous utiliserons donc le terme octet la plupart du temps dans la suite de ce wikilivre.

Un autre piège de ce type est qu'il peut être de base (c'est-à-dire implicitement) signed ou unsigned, au choix du compilateur, ce qui peut s'avérer dangereux (c'est-à-dire difficile à maitriser). Considérez le code suivant :

/* Ce code peut ne pas fonctionner avec certains compilateurs */
char i;
for (i = 100; i >= 0; i --)
{
    /* ... */
}

Dans cet exemple, l'instruction for permet de faire itérer les valeurs entières de i de 100 à 0, incluses. On pourrait naïvement penser optimiser en utilisant un type char. Sauf que si ce type est implicitement unsigned, la condition i >= 0 sera toujours vraie, et tout ce que vous obtiendrez est une boucle infinie. Normalement, tout bon compilateur devrait vous avertir que la condition est toujours vraie et donc vous permettre de corriger en conséquence, plutôt que perdre des heures en débugage.

Constantes représentant un caractère

Une constante représentant un caractère (de type char) est délimitée par des apostrophes, comme par exemple 'a'. En fait pour le C, les caractères ne sont ni plus ni moins que des nombres entiers (ils sont de type int, mais leur valeur tiendra dans un type char), les deux étant parfaitement interchangeables. Cependant, comme dit plus haut, il ne s'agit généralement pas là des caractères Unicode que nous manipulons tous les jours, mais de caractère-octet dont la portée est réduite.

Un petit exemple :

#include <stdio.h>

int main(void)
{
    printf("Sur votre machine, la lettre 'a' a pour code %d.\n", 'a');
    return 0;
}

Le programme précédent donnera le résultat suivant dans un environnement ASCII :

Sur votre machine, la lettre 'a' a pour code 97.

Il est tout à fait autorisé d'écrire 'a' * 2 ou 'a' - 32.

Cette soustraction permet de convertir une minuscule ASCII en majuscule si le codage ASCII est utilisé, mais ne le permet pas si un autre codage est utilisé (comme ISO-8859-1 ou UTF-8).

Valeur des caractères-octets

La valeur représentée par cette constante est néanmoins dépendante des conventions de codages de caractères employées. À l'origine ces conventions étaient connues du système ; aujourd'hui, même si dans une écrasante majorité des cas, on se retrouvera avec un jeu de caractères augmentant l'ASCII et donc l'ISO-646. La code ASCII définit 96 glyphes de caractères portant les numéros 32 à 126, bien loin des milliers de caractères nécessaires pour les logiciels fonctionnant sur la planète entière et dans des langues nombreuses[10]. Cet ensemble est à peine suffisant pour couvrir l'anglais alors que plusieurs langues latines étaient visées à l'époque où l'ASCII et l'ISO-646 ont été définis, si bien que de nombreuses extensions sont par la suite apparues.

Par exemple le caractère « œ » (ligature du o et du e) a pour valeur :

  • 189 dans le jeu de caractères ISO-8859-15 (principalement utilisé pour les langues latines d'Europe, sous Unix) ;
  • 156 sur certaines variantes du jeu de caractères Windows 1252 (principalement utilisé pour les langues latines d'Europe, sous Windows) ;
  • 0xc5, 0x93 (deux octets, donc deux char) en l'UTF-8 ;
  • 207 avec l'encodage Mac Roman (Mac OS 9 et antérieur) ;
  • Et n'a pas d'équivalent ni en ISO-8859-1 ni en ASCII (le caractère 189 est le symbole « ½ », le 207 est le « Ï » et le 156 n'est pas utilisé).

Retenons simplement qu'Unicode se développe sur Internet, mais que des logiciels plus anciens ou embarqués peuvent fonctionner avec des jeux de caractères huit bits, plus limités. Il s'agit en fait d'une problématique concernant des sujets plus vastes, comme l' internationalisation, la portabilité et l' interopérabilité.

Le langage C reste relativement agnostique à ce niveau : les caractères sont des octets et les chaînes, une simple suite d'octets terminée par 0. Il laisse au système et au développeur le soin d'interpréter et de traiter les octets comme il se doit. Ceci peut être fait à l'aide de bibliothèques appropriées. Si on trouve ici où là de telles listes de bibliothèques [11], il est préférable de connaître le langage C avant de les utiliser.

Cet agnosticisme du langage C lui a permis de s'adapter aux nombreuses évolutions des conventions de codages des caractères.

Caractères non graphiques

Certains caractères ne sont pas graphiques. Par définition, on ne peut pas donc les écrire dans un code source de manière visible. Le langage C adopte la convention suivant pour désigner certains d'entre eux de manière littérale :

Constante | Caractère
'\'' | une apostrophe
'\"' | un guillemet
'\?' | un point d'interrogation
'\\' | un backslash
'\a' | un signal sonore (ou visuel)
'\b' | un espace arrière
'\f' | saut au début de la page suivante
'\n' | saut de ligne
'\r' | un retour chariot
'\t' | une tabulation
'\v' | une tabulation verticale

De plus, on peut écrire n'importe quelle valeur de caractère avec les expressions suivantes :

  • '\xHH', où chaque H représente un chiffre hexadécimal correspondant au code du caractère. Par exemple, '\x61' représente le caractère 'a' (minuscule) en ASCII (car 97 = 6 * 16 + 1) ;
  • "\xc5\x93" représente le caractère œ en UTF-8.
  • '\oOO', où chaque O représente un chiffre octal correspondant au code du caractère.

Trigraphe

Le langage C date d'une époque où le standard de codage des caractères était l'ISO-646, un standard encore plus ancien et plus incomplet que l'ASCII. Les caractères absent de l'ISO-646 pouvaient être simulés par une séquence de trois caractères.

L'ASCII s'étant répandu, cette fonctionnalité tombée en désuétude est en général à éviter, car très souvent inutile, et peut causer des bugs incompréhensibles au programmeur non averti. Néanmoins, de très rares programmes peuvent utiliser ce genre de fonctionnalités. Un trigraphe est simplement une suite de trois caractères dans le code source qui sera remplacée par un seul.

Cette fonctionnalité a été ajoutée au C pour supporter les architectures (systèmes) dont l'alphabet ne dispose pas de certains caractères qui sont nécessaires dans la syntaxe du C, comme les dièses ou les accolades.

Les substitutions suivantes sont faites partout dans le code source (y compris les chaînes de caractères) :

Trigraphe | Caractère
??= | #
??( | [
??) | ]
??< | {
??> | }
??/ | \
??' | ^
??! | |
??- | ~

Voici une manière de rendre illisible un programme utilisant les trigraphes :

??=include <stdio.h>

int main(void)
??<
   puts("Bonjour !");
   return 0;
??>

Par défaut, la plupart des compilateurs désactivent les trigraphes, au cas où vous ne seriez pas encore dissuadé de les utiliser.


Chaîne de caractères

Une chaîne de caractère, comme son nom l'indique, est une suite de caractères avec la particularité d'avoir un caractère nul (0) à la fin. Une chaîne de caractère est en fait implémentée en C avec un tableau de type char.

Ce chapitre vous donne les rudiments sur les chaînes de caractère pour une première approche. Pour en savoir plus sur

  • les rares fonctions de chaînes de caractères, voir Programmation C/Chaînes de caractères
  • les tableaux & pointeurs : Programmation_C/Tableaux#Cha.C3.AEnes_de_caract.C3.A8res
Programmation_C/Tableaux#Chaînes_de_caractères

Structure

En mémoire, une chaîne de caractère est représenté dans le langage C comme un tableau d'octet dont les valeurs dépendent de l'encodage utilisé ici, Unicode.

Amélie en Unicode formes NFC et NFD
Caractère représenté | A | m | é |  | l | i | e | '\0'
Unicode NFC | 0041 | 006d | 00e9 |  | 006c | 0069 | 0065 | 0
Unicode UTF-8 NFC | 41 | 6d | c3 a9 |  | 6c | 69 | 65 | 0
Unicode NFD | 0041 | 006d | 0065 | 0301 | 006c | 0069 | 0065 | 0
Unicode UTF-8 NFD | 41 | 6d | 65 | cc 81 | 6c | 69 | 65 | 0
Unicode NFD | A | m | e | ◌́ | l | i | e | '\0'

Chaîne littérale

On appelle une chaîne littérale la manière dont est définie une constante chaîne dans un code sources. Sous sa forme la plus simple, on déclare une chaîne comme une suite de caractères entre guillemets (double quote) :

"Ceci est une chaîne de caractère";
""; /* Chaîne vide */

Si la chaîne est trop longue, on peut aussi la couper sur plusieurs lignes :

"Ceci est une chaîne de caractère, " /* pas de ; */
"déclarée sur plusieurs lignes.";

Deux chaînes côtes à côtes (modulo les espaces et les commentaires) seront concaténées par le compilateur. De plus on peut utiliser le caractère barre oblique inverse (\) pour annuler la signification spéciale de certains caractères ou utiliser des caractères spéciaux (C.f liste ci-dessus).

"Une chaîne avec des \"guillemets\" et une barre oblique (\\)\n";

Il est aussi possible d'utiliser la notation hexadécimale et octale pour décrire des caractéres dans la chaîne, il faut néanmoins faire attention avec la notation hexadécimale, car la définition peut s'étendre sur plus de 2 caractéres. En fait, tous les caractères suivant le \x et appartenant à l'ensemble "0123456789abcdefABCDEF" seront utilisés pour déterminer la valeur du caractère (du fait que les caractères d'une chaîne peuvent faire plus qu'un octet). Cela peut produire certains effets de bords comme :

/* Ce code contient un effet de bord inattendu */
"\x00abcdefghijklmnopqrstuvzxyz"

Ce code n'insèrera pas un caractère 0 au début de la chaine, mais toute la séquence "\x00abcdef" servira à calculer la valeur du caractère (même si le résultat sera tronqué pour tenir dans un type char). On peut éviter cela en utilisant la concaténation de chaînes constantes :

"\x00" "abcdefghijklmnopqrstuvzxyz"

Enfin, les chaînes de caractères faisant appel aux concepts de pointeur, tableau et de zone mémoire statique, leur utilisation plus poussée sera décrite dans la section dédiée aux tableaux.

À noter la représentation obsolète des chaînes de caractères multilignes. À éviter dans la mesure du possible :

/* Ce code est obsolete et a eviter */
"Ceci est une chaîne de caractère,\
déclarée sur plusieurs lignes";

Booléens

Le langage (jusqu'à la norme C99) ne fournit pas de type booléen. La valeur entière 0 prend la valeur de vérité faux et toutes les autres valeurs entières prennent la valeur de vérité vrai.

La norme C99 a introduit le type _Bool, qui peut contenir les valeurs 0 et 1. Elle a aussi ajouté l'en-tête <stdbool.h>, qui définit le type bool qui est un raccourci pour _Bool, et les valeurs true et false[12]. Néanmoins, ces nouveautés du C99 ne sont pas très utilisées, les habitudes ayant été prises d'utiliser 0 et différent de zéro pour les booléens en C.

Nota : toute expression utilisant des opérateurs booléens (voir opérateurs), retourne 1 si l'expression est vraie et 0 si elle est fausse, ce qui rend quasiment inutile l'usage du type booléen.

Vide

En plus de ces types, le langage C fournit un autre type, void qui représente rien, le vide. Il n'est pas possible de déclarer une variable de type void. Nous verrons l'utilité de ce type lorsque nous parlerons de fonctions et de pointeurs.


Qualificateurs

Le C définit trois qualificateurs pouvant influer sur une variable :

  • const : pour définir une variable dont la valeur ne devrait jamais changer ;
  • restrict : permet une optimisation pour la gestion des pointeurs ;
  • volatile : désigne une variable pouvant être modifiée notamment par une source externe indépendante du programme.

Une variable, ou un paramètre de fonction, peut avoir aucun, un, deux, ou les trois qualificateurs (certaines combinaisons n'auraient que peu de sens, mais sont autorisées).

Qualificateur 'const'

La classe const ne déclare pas une vraie constante, mais indique au compilateur que la valeur de la variable ne doit pas changer. Il est donc impératif d'assigner une valeur à la déclaration de la variable, sans quoi toute tentative de modification ultérieure entrainera une erreur de la part du compilateur :

Avertissement Ce code contient une erreur volontaire !
const int i = 0;
i = 1; /* erreur*/

En fait, le mot-clé const est beaucoup plus utilisé avec des pointeurs. Pour indiquer qu'on ne modifie pas l'objet pointé, il est bon de spécificier le mot-clé const :

void fonction( const char * pointeur )
{
        pointeur[0] = 0;     /* erreur*/
        pointeur    = "Nouvelle chaine de caractères";
}

Dans cet exemple, on indique que l'objet pointé ne sera pas modifié. Pour indiquer que la valeur elle-même du pointeur est constante, il faut déclarer la variable de la sorte :

Avertissement Ce code contient une erreur volontaire !
char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/

Encore plus subtil, on peut mélanger les deux :

Avertissement Ce code contient plusieurs erreurs volontaires !
const char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/
pointeur[0] = 0; /* erreur*/

Cette dernière forme est néanmoins rarement usitée. En outre ce dernier exemple présente un autre problème qui est la modification d'une chaine de caractères « en dur », qui sont la plupart du temps placées dans la section lecture seule du programme et donc inaltérables.

Qualificateur 'volatile'

Ce mot-clé sert à spécifier au compilateur que la variable peut être modifiée à son insu. Cela annule toute optimisation que le compilateur pourrait faire, et l'oblige à procéder à chaque lecture ou écriture dans une telle variable tel que le programmeur l'a écrit dans le code. Ceci a de multiples utilisations :

  • pour les coordonnées d'un pointeur de souris qui seraient modifiées par un autre programme ;
  • pour la gestion des signaux (voir Gestion des signaux) ;
  • pour de la programmation avec de multiples fils d'exécution qui doivent communiquer entre eux ;
  • pour désigner des registres matériels qui peuvent être accédés depuis un programme C (une horloge, par exemple), mais dont la valeur peut changer indépendamment du programme ;
  • etc.

On peut combiner const et volatile dans certaines situations. Par exemple :

extern const volatile int horloge_temps_reel;

déclare une variable entière, qu'on ne peut modifier à partir du programme, mais dont la valeur peut changer quand même. Elle pourrait désigner une valeur incrémentée régulièrement par une horloge interne.

Qualificateur 'restrict'

Introduit par C99, ce mot-clé s'applique aux déclarations de pointeurs uniquement. Avec restrict, le programmeur certifie au compilateur que le pointeur déclaré sera le seul à pointer sur une zone mémoire. Cela permettra au compilateur d'effectuer des optimisations qu'il n'aurait pas pu deviner autrement. Le programmeur ne doit pas mentir sous peine de problèmes...

Par exemple :

int* restrict pZone;


Classes de stockage

Il est possible de construire des types dérivés des types de base du langage en utilisant plusieurs combinaisons, deux étant illustrées dans ce chapitre: les classes de stockage et les qualificateurs.

Classe de stockage

Le langage C permet de spécifier, avant le type d'une variable, un certain nombre de classes de stockage :

  • auto : pour les variables locales ;
  • extern : déclare une variable sans la définir ;
  • register : demande au compilateur de faire tout son possible pour utiliser un registre processeur pour cette variable ;
  • static : rend une définition de variable persistante.

Les classes static et extern sont, de loin, les plus utilisées. register est d'une utilité limitée, et auto est maintenant obsolète.

Une variable, ou un paramètre de fonction, ne peut avoir qu'au plus une classe de stockage.

Classe 'static'

L'effet de la classe 'static' dépend de l'endroit où l'objet est déclaré :

  • Objet local à une fonction : la valeur de la variable sera persistante entre les différents appels de la fonction. La variable ne sera visible que dans la fonction, mais ne sera pas réinitialisée à chaque appel de la fonction. L'intérêt est de garantir une certaine encapsulation, afin d'éviter des usages multiples d'une variable globale. Qui plus est, cela permet d'avoir plusieurs fois le même nom, dans des fonctions différentes.

Exemple :

#include <stdio.h>

void f(void)
{
    static int i = 0; /* i sera initialisée à 0 à la compilation seulement */
    int j = 0; /* j sera initialisée à chaque appel de f */;
    i++;
    j++;
    printf("i vaut %d et j vaut %d.\n", i, j);
}

int main(void)
{
    f();
    f();
    f();
    return 0;
}

Résultat d'exécution du code ci dessus :

i vaut 1 et j vaut 1.
i vaut 2 et j vaut 1.
i vaut 3 et j vaut 1.
  • Objet global et fonction : comme une variable globale est déjà persistante, le mot-clé static aura pour effet de limiter la portée de la variable ou de la fonction au seul fichier où elle est déclarée, toujours dans le but de garantir un certain niveau d'encapsulation.

Une variable de classe statique est initialisée au moment de la compilation à zéro par défaut (contrairement aux variables dynamiques qui ont une valeur initiale indéterminée). Elle peut être initialisée explicitement à n'importe quelle valeur constante.

À faire...
utilisation de static en C99 pour les tableaux en paramètres de fonctions

Classe 'extern'

extern permet de déclarer une variable sans la définir. C'est utile pour la compilation séparée, pour définir une variable ou une fonction dans un fichier, en permettant à des fonctions contenues dans d'autres fichiers d'y accéder.

Toutes les variables globales et fonctions qui ne sont pas déclarées (ou définies) static sont externes par défaut.

static et extern sont employés pour distinguer, dans un fichier C, les objets et fonctions « privés », qui ne doivent être utilisés que depuis l'intérieur du fichier, et ceux qui sont « publics », et pourront être accessibles depuis d'autres fichiers.

Classe 'register'

L'usage de ce mot clé est utile dans un contexte de logiciel embarqué. Indique que la variable devrait être stockée dans un registre du processeur. Cela permet de gagner en performance par rapport à des variables qui seraient stockées dans un espace mémoire beaucoup moins rapide, comme une pile placée en mémoire vive.

Ce mot-clé a deux limitations principales :

  • Les registres du processeur sont limités. Leur nombre peut varier en fonction du processeur. Sur un PC (d'architecture AMD64?), il sont au nombre de 13, dont seulement 4 servent au stockage (EAX,EBX,ECX,EDX). Il est donc inutile de déclarer une structure entière ou un tableau avec le mot clé register.
  • Qui plus est, les variables placées dans des registres sont forcément locales à des fonctions ; on ne peut pas définir une variable globale en tant que registre.

Aujourd'hui, ce mot-clé est déconseillé sauf pour des cas particuliers, les compilateurs modernes sachant généralement mieux que le programmeur comment optimiser et quelles variables placer dans les registres.

#include <stdio.h>

int main(void)
{
    register short i, j;
    for (i = 1; i < 1000; ++i)
    {
        for(j = 1; j < 1000; ++j)
        {
            printf("\n %d %d", i, j);
        }
    }
    return 0;
}

Classe 'auto'

Cette classe est un héritage du langage B. En C, ce mot-clé sert pour les variables locales à une fonction non-statiques, dites aussi automatiques. Mais une variable déclarée localement à une fonction sans qualificateur static étant implicitement automatique, ce mot-clé est inutile en C.


Opérateurs

Les opérateurs du C

Les opérateurs du C permettent de former des expressions, expressions qui diront quoi faire à votre programme. On peut voir un programme C comme étant composé de trois catégories d'instructions :

  • Les déclarations et définitions (variables, fonctions, types) : déclarent et définissent les objets que pourront manipuler le programme.
  • Les expressions : manipulent les déclarations, via les opérateurs.
  • Les instructions : manipulent les expressions pour leur donner une signification particulière (test, boucle, saut, ...).

Les déclarations de variables ont en partie été décrites dans les chapitres précédents. Les expressions seront en partie décrites dans celui-ci (les opérateurs liés aux pointeurs, tableaux, structures et fonctions seront décrit dans des chapitres dédiés), et les instructions seront décrites au cours des chapitres suivants.

Les expressions en C sont en fait très génériques, elles peuvent inclure des opérations arithmétiques classiques (addition, soustraction, division, modulo, ...), des expressions booléennes (OU logique, ET logique, OU exclusif, ...), des comparaisons (égalité, inégalité, différence, ...) et même des affectations (copie, auto-incrément, ...). Toutes ces opérations peuvent s'effectuer en une seule expression, il suffit d'appliquer les bons opérateurs sur les bons opérandes.

Les opérateurs binaires et ternaires utilisent une notation infixe (l'opérateur se trouve entre les 2 ou 3 opérandes). Les opérateurs unaires s'écrivent de manière préfixé (avant l'opérande), à l'exception des opérateurs ++ et -- qui peuvent s'écrire de manière préfixée ou suffixée (avec une différence subtile, décrite ci-après).

La priorité (quel opérateur est appliqué avant, en l'absence de parenthèses explicite) et l'associativité (dans quel ordre sont traités les arguments des opérateurs ayant la même priorité) sont résumées dans la table suivante (par ordre décroissant de priorité - les opérateurs décrits dans un autre chapitre ont un lien dédié) :

opérateur | parité | associativité | description
( ) |  | gauche vers la droite (GD) | parenthésage
() [] . -> |  | GD | appel de fonction, index de tableau, membre de structure, pointe sur membre de structure
! | unaire | droite vers la gauche (DG) | négation booléenne
~ | unaire | DG | négation binaire
++ -- | unaire | DG | incrémentation et décrémentation
- | unaire | DG | opposé
(type) | unaire | DG | opérateur de transtypage (cast)
* | unaire | DG | opérateur de déréférençage
& | unaire | DG | opérateur de référençage
sizeof | unaire | DG | fournit la taille en nombre de "char" de l'expression (souvent en octet mais pas toujours, mais sizeof(char) == 1 par définition, voir Caractères)
* / % | binaire | GD | multiplication, division, modulo (reste de la division)
+ - | binaire | GD | addition, soustraction
>> << | binaire | GD | décalages de bits
> >= < <= | binaire | GD | comparaisons
== != | binaire | GD | égalité/différence
& | binaire | GD | et binaire
^ | binaire | GD | ou exclusif binaire
| | binaire | GD | ou inclusif binaire
&& | binaire | GD | et logique avec séquencement
|| | binaire | GD | ou logique avec séquencement
? : | ternaire | DG | si...alors...sinon
= += -= *= /= %= ^= &= |= >>= <<= | binaire | DG | affectation
, | binaire | GD | séquencement

Post/pré incrémentation/décrémentation

C'est un concept quelque peu sibyllin du C. Incrémenter ou décrémenter de un est une opération extrêmement courante. Écrire à chaque fois 'variable = variable + 1' peut-être très pénible à la longue. Le langage C a donc introduit des opérateurs raccourcis pour décrémenter ou incrémenter n'importe quel type atomique (gérable directement par le processeur : c'est à dire pas par un tableau, ni une structure ou une union). Il s'agit des opérateurs '++' et '--', qui peuvent être utilisés de manière préfixée ou suffixée (avant ou après la variable).

Utilisé de manière préfixée, l'opérateur incrémente/décrémente la variable, puis retourne la valeur de celle-ci. En fait, les expressions (++E) et (--E) sont équivalentes respectivement aux expressions (E+=1) et (E-=1).

Par contre, utilisé de manière suffixée, l'opérateur retourne la valeur originale avant de modifier la valeur de la variable.

int i = 0, j;

j = i++; /* j vaut  0 et i vaut  1 */
j = --i; /* j vaut 0 et i vaut 0 */

Il est important de noter que le langage ne fait que garantir qu'une variable post-incrémentée ou post-décrémentée acquiert sa nouvelle valeur entre le résultat de l'opérateur et le prochain point de séquencement atteint (généralement la fin d'une instruction). Mais on ne sait pas vraiment quand entre les deux. Ainsi, si l'objet sur lequel s'applique un tel opérateur apparaît plusieurs fois avant un point de séquencement, alors le résultat est imprévisible et son comportement peut changer simplement en changeant les options d'un même compilateur :

Avertissement Ce code contient plusieurs erreurs volontaires !
/* Le code qui suit est imprévisible */
int i = 0;
i = i++;
Avertissement Ce code contient une erreur volontaire !
/* Et celui-là d'après vous ? */
int i = 0;
i = ++i;

Contrairement à ce à quoi vous pouviez vous attendre, le bout de code ci-dessous n'est pas mieux défini que celui d'au-dessus. La valeur de i y est modifiée une première fois lors de son incrémentation, puis une deuxième fois par l'opérateur d'affectation =. Or, un autre moyen de générer un comportement indéfini est de modifier la valeur d'une variable plusieurs fois entre deux points de séquencement.

Promotion entière

La promotion entière, à ne pas confondre avec la conversion automatique de type, fait que tous les types plus petits ou égaux à int (char, short, champs de bits, type énuméré) sont convertis (promus) en int ou unsigned avant toute opération. Ainsi, dans l'exemple suivant, a + b est calculé avec des int et le résultat est de type int :

short sum(short a, short b) {
    return a + b; /* équivaut à : return (int)a + (int)b; */
}

Le compilateur peut émettre un avertissement du fait que le résultat int est converti en short pour être retourné, et cette conversion peut causer une perte de précision (par exemple si int a une largeur de 32 bits et short de 16 bits).

La promotion se fait vers unsigned lorsqu'un int ne peut pas représenter le type promu.

Conversion automatique

La conversion est un mécanisme qui permet de convertir implicitement les nombres dans le format le plus grand utilisé dans l'expression.

L'exemple classique est le mélange des nombres réels avec des nombres entiers. Par exemple l'expression '2 / 3.' est de type double et vaudra effectivement deux tiers (0,666666...). L'expression '2 * a / 3' calculera les deux tiers de la variable 'a', et arrondira automatiquement à l'entier par défaut, les calculs ne faisant intervenir que des instructions sur les entiers (en supposant que 'a' soit un entier). À noter que l'expression '2 / 3 * a' vaudra... toujours zéro !

Plus subtile, l'expression '2 * a / 3.', en supposant toujours que a soit un entier, effectuera une multiplication entière entre 2 et a, puis promouvra le résultat en réel (double) puis effectuera la division réelle avec trois, pour obtenir un résultat réel lui-aussi.

Enfin un cas où les promotions peuvent surprendre, c'est lorsqu'on mélange des entiers signés et non-signés, plus particulièrement dans les comparaisons. Considérez le code suivant :

/* Ce code contient un effet de bord sibyllin */
unsigned long a = 23;
signed char b = -23;

printf( "a %c b\n", a < b ? '<' : (a == b ? '=' : '>') );

De toute évidence a est supérieur à b dans cet exemple. Et pourtant, si ce code est exécuté sur certaines architectures, il affichera a < b, justement à cause de cette conversion.

Tout d'abord dans la comparaison a < b, le type de a est « le plus grand », donc b est promu en unsigned long. C'est en fait ça le problème : -23 est une valeur négative, et la conversion d'une valeur négative en un type non signé se fait modulo la valeur maximale représentable par le type non signé + 1. Si la valeur maximale pour le type unsigned long est 232-1, alors -23 est converti modulo 232, et devient 4294967273 (soit 232-23). Dans ce cas là effectivement 23 < 4294967273, d'où ce résultat surprenant.

À noter qu'on aurait eu le même problème si le type de b était signed long. En fait les types signés sont promus en non-signés, s'il y a au moins une variable non-signée dans l'expression, et que le type non-signé est "plus grand" que le type signé.

Dans ce cas présent, il faut effectuer soi-même le transtypage :

printf( "a %c b\n", (long) a < b ? '<' : ((long)a == b ? '=' : '>') );

La norme définit précisément les conversions arithmétiques et les promotions entières, se référer à elle pour les détails.

Évaluation des expressions booléennes

Le C ne possède pas de type booléen dédié[13]. Dans ce langage, n'importe quelle valeur différente de zéro est considérée vraie, zéro étant considéré comme faux. Ce qui veut dire que n'importe quelle expression peut être utilisée à l'intérieur des tests (entier, réels, pointeurs, tableaux, etc.). Cela peut conduire à des expressions pas toujours très claires, comme :

int a;
a = une_fonction();
if (a)
{
    /* ... */
}

Ce type d'écriture simplifiée a ses adeptes, mais elle peut aussi s'écrire de la manière suivante:

int a;
a = une_fonction();
if (a != 0)
{
    /* ... */
}

Cette seconde écriture, sémantiquement équivalente à la première, rend explicite le test qui est effectué (on compare la valeur de la variable a à zéro).

Par ailleurs, cette absence de type booléen rend aussi valide le code suivant:

int a = 0;
int b = 2;

if (a = b)
{
        /* Le code qui suit sera toujours exécuté ... */
}

Dans cet exemple, il y a un seul caractère = entre a et b, donc on ne teste pas si a est égal à b, mais on affecte la valeur de la variable b à la variable a. Le résultat de l'expression étant 2, elle est donc toujours considérée vraie. Ce genre de raccourci est en général à éviter. En effet, il est facile de faire une faute de frappe et d'oublier de taper un caractère =. Comme le code résultant est toujours valide au sens du C, cette erreur peut ne pas être vue immédiatement. Pour aider les développeurs à détecter ce genre d'erreurs, de nombreux compilateurs émettent un avertissement quand un tel code leur est présenté. Une manière de faire taire ces avertissements, une fois qu'on s'est assuré qu'il ne s'agit pas d'une erreur, est de mettre entre parenthèses l'affectation:

/* On veut faire une affectation ici. */
/* doublement des parenthèses pour supprimer l'avertissement du compilateur*/
if ((a = b))
{
    /* ... */
}

Comme un développeur devant maintenir un programme contenant un test if (a = b) { /* ... */ } se demandera si le développeur précédent n'a pas fait une faute de frappe, il est préférable d'éviter autant que possible ce genre de situations et, s'il est nécessaire (ce qui a très peu de chances d'arriver), de la commenter. En effet, même un code if ((a = b)) { /* ... */ } non commenté doit être lu avec attention, car le développeur précédent peut avoir ajouté les parenthèses juste pour faire taire le compilateur, sans se rendre compte d'une erreur. Dans tous les cas, la manière la plus sûre est de décomposer ainsi:

a = b;
if (a != 0)
{
    /* ... */
}

Une autre technique classique, lorsqu'une comparaison fait intervenir une constante, est de mettre la constante à gauche. De cette manière, si la comparaison se transforme par mégarde en affectation, cela provoquera une erreur à la compilation :

if (0 == b)
{
    /* Une instruction "0 = b" ne passerait pas */
}

Les opérateurs logiques de comparaisons (&& et ||, similaires sémantiquement à leur équivalent binaire & et |) évaluent leurs opérandes en circuit court. Dans le cas du ET logique (&&), si l'opérande gauche s'évalue à faux, on sait déjà que le résultat du ET sera faux et donc ce n'est pas la peine d'évaluer l'opérande droite. De la même manière si l'opérande gauche d'un OU logique (||) est évalué à vrai, le résultat sera aussi vrai et donc l'évaluation de l'opérande droite est inutile. Ceci permet d'écrire des expressions de ce genre, sans générer d'exception de l'unité arithmétique du processeur :

if (z != 0 && a / z < 10)
{
    printf("Tout va bien\n");
}

Et voila !

On réduit :

if (string)
 len = strlen(string);
else
 len = 0;

par

len = string ? strlen(string) : 0;

C'est à dire que len égale si string à la longueur de string si non 0 ;)

L'ordinateur calcule très vite et très bien, c'est pour ça que l'utiliser reste un atout.

Les calculs ne sont pour lui que des instructions simples et utilisant peu de ressource (mul, div, add, sub, inc, xor, ...).


Structures de contrôle - tests

Les tests permettent d'effectuer des opérations différentes suivant qu'une condition est vérifiée ou non.

Test if

Les sauts conditionnels permettent de réaliser une instruction si une condition est vérifiée. Si la condition n'est pas vérifiée, l'exécution se poursuit séquentiellement.

Syntaxe

Première forme :

/* Ce code est volontairement incomplet */
    if (condition)
        instruction
    instruction suivante

Si la condition est vérifiée, alors on exécute l'instruction, sinon on exécute l'instruction suivante.

/* Ce code est volontairement incomplet*/
    if (condition) instruction; /* ... */
    instruction suivante; /* ... */ ;
Deuxième forme

Deuxième forme : if ( condition )

{

instructions

}else

/* Ce code est volontairement incomplet */
    if (condition)
        instructions
    instruction suivante

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute exécute l'instruction suivante.

/* Ce code est volontairement incomplet */
    if (condition)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    }
    instruction suivante; /* ... */ ;
Troisième forme

Troisième forme :

/* Ce code est volontairement incomplet */
    if (condition)
        instructions
    else
        autre instructions
    instruction suivante; /* ... */ ;

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute l'autre bloc.

    /* Ce code est volontairement incomplet*/
    if (condition)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    } else {
       instruction 3; /* ... */
       instruction 4; /* ... */
    }
    instruction suivante; /* ... */ ;

Quatrième forme :

    /* Ce code est volontairement incomplet*/
    if (condition 1)
        instructions
    else if (condition 2)
        autre instructions
    else
        autre instructions
    instruction suivante; /* ... */ ;

Si la condition est vérifiée, alors on exécute le bloc d'instructions, sinon on exécute l'autre if comme la troisième forme.

/* Ce code est volontairement incomplet */
    if (condition 1)
    {
       instruction 1; /* ... */
       instruction 2; /* ... */
    } else if (condition 2) {
       instruction 3; /* ... */
       instruction 4; /* ... */
    } else {
       instruction 5; /* ... */
       instruction 6; /* ... */
    }
    instruction suivante; /* ... */ ;

Exemple de code :

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int a;
    a = 3; /* assignation d'une valeur pour l'exemple*/

    if (a >= 5)
        printf("Le nombre a est supérieur ou égal à 5 \n");
    else
        printf("Le nombre a est inférieur à 5 \n");
    return 0;
}

D'ailleurs, lorsqu'un if contient une instruction (for, do, while, switch, goto, return, etc.) il est conseillé de la mettre entre accolades.

Test switch

Cette instruction permet de tester si une expression coïncide avec un certain nombre de constantes, et d'exécuter une action par défaut dans le cas où aucune valeur ne correspond à celle de l'expression. Cela permet un traitement beaucoup plus efficace et lisible qu'une succession de if/else imbriqués. Insistons sur le terme constante : il est en effet impossible d'utiliser des expressions dont la valeur n'est pas connue à la compilation (c'est à dire de variable dans les instructions case).

Syntaxe

switch (expression)
{
    case valeur1:
        bloc1
    case valeur2:
        bloc2
    /*...*/
    case valeurN:
        blocN
    default:
        blocD
}

Compare la valeur de l'expression à celles de valeur1, valeur2, ..., valeurN. En cas d'égalité entre expression et valeurI les blocs sont exécutés séquentiellement à partir de blocI, et ce jusqu'à la fin de l'instruction switch. Si expression est égale à valeur2, dans cet exemple, les blocs bloc2 à blocN et même blocD seront exécutés. Pour empêcher ce comportement on utilise l'instruction break, que l'on peut placer à n'importe quel endroit pour sortir (aller à la fin) de l'instruction switch. En général, on retrouve plus fréquemment l'instruction switch écrite de la sorte :

switch (expression)
{
    case valeur1:
        bloc1
        break;
    case valeur2:
        bloc2
        break;
    /*...*/
    case valeurN:
        blocN
        /* pas de break; */
    default:
        blocD
}

C'est en fait tellement rare de ne pas mettre de break entre les différents cas, qu'il est conseillé de mettre un commentaire pour les cas où cette instruction est délibérément omise, ça permet de bien signaler au lecteur qu'il ne s'agit pas d'un oubli.

Expression conditionnelle

condition ? expression_si_vrai : expression_si_faux ;

Exemple

#include <stdio.h>

int main(int argc, char * argv[])
{
    /*
        program                      argc=1  argv[0]="program"
        program argument             argc=2  argv[0]="program"  argv[1]="argument"
        program argument argument    argc=3  argv[0]="program"  argv[1]="argument"  argv[2]="argument"
        ...
    */
    printf("%s\n", argc < 2 ? "Vous n'avez pas donné d'argument." : "Vous avez donné au moins un argument.");
    return 0;
}

Ce mini-programme teste si le nombre d'arguments passé à main est inférieur à 2 avec l'expression argc < 2 et renvoie "vous n'avez pas donné d'argument" si l'expression est vraie et "vous avez donné au moins un argument" sinon. Le résultat de l'évaluation est alors passé à la fonction printf qui affiche le résultat. (si argc vaut 2 alors il y a 1 argument : voir "la fonction main" dans le chapitre "fonctions")

On note le manque de clarté de l'exemple: l'opérateur ternaire ne doit être utilisé que dans de rares cas, c'est à dire lorsque son utilisation ne se fait pas au détriment de la lisibilité du code.


Structures de contrôle - itérati

Structures de contrôle - itérations

Boucle for

Syntaxe

for (initialisation ; condition ; itération)
    bloc

Une boucle for commence par l' initialisation, puis exécute le bloc de code tant que la condition est vérifiée et en appelant l'itération après chaque bloc.

On peut tout de suite remarquer que la boucle for du C est beaucoup plus puissante qu'une boucle for classique, dans le sens où elle ne correspond pas forcément à un nombre fini de tours de boucle.

La norme C99 permet maintenant de déclarer des variables dans la partie initialisation de la boucle for. Ces variables ne peuvent qu'être de classe automatic ou register. Leur portée est limitée à la boucle. Elles n'interfèrent pas avec les variables de même nom déclarées en dehors de la boucle. Leur utilisation pourrait conduire à de possibles optimisations du code par le compilateur.

Exemples

Exemple 1
int i;
for (i = 0 ; i < 10 ; i++)
{
    printf("%d\n", i);
}

En C++ :

for (int i = 0 ; i < 10 ; i++)
{
    printf("%d\n", i);
}

Cet exemple est une boucle for normale, l'indice de boucle i est incrémenté à chaque tour et le bloc ne fait qu'afficher l'indice. Ce code affiche donc les 10 premiers entiers en partant de 0. Le bloc de code étant réduit à une instruction, on aurait pu écrire :

int i;
for (i = 0 ; i < 10 ; i++)
    printf("%d\n", i);

Exemple 2

int i = 0;
for( ; i != 1 ; i = rand() % 4)
{
        printf("je continue\n");
}

Ce deuxième exemple montre plusieurs choses :

  1. l'initialisation de la boucle a été omise, en fait n'importe laquelle des trois parties peut l'être (et même les trois à la fois, ce qui résulte en une boucle infinie) ;
  2. l'expression itération est réellement une expression, on peut y faire appel à des fonctions ;
  3. le nombre d'itérations n'est pas fixe, le programme affichera « je continue » un nombre (pseudo-)aléatoire de fois.
for(;;) crée une boucle infinie.

Boucle while

Syntaxe

while (condition)
    bloc

Une boucle while exécute le bloc d'instructions tant que la condition est vérifiée.

Exemple

int i = 0;
while (i < 10)
{
        printf("%d\n", i);
        i = i + 1;
}

Cet exemple est le même que celui vu précédement pour la boucle for, il affiche les dix premiers entiers à partir de 0.

Boucle do-while

Syntaxe

do
    bloc
while (condition);

Cette boucle est une légère variante de la précédente. Elle exécute donc le bloc au moins une fois, et ce jusqu'à ce que la condition soit fausse (ou tant que la condition est vraie).

Exemple 1

#include <stdio.h>

int main(void)
{
    int i = 0;
    do
    {
        printf("%d\n", i);
        i = i + 1;
    } while (i < 10);
    return 0;
}

Exemple identique à celui de la boucle while au dessus, on affiche les nombres de 0 jusqu'à 9.

Exemple 2

#include <stdio.h>

int main(void)
{
    char c;
    do
    {
        printf("Veuillez taper la lettre 'o'\n");
        scanf("%c", &c);
    } while (c != 'o');
    return 0;
}

Cette fois on voit l'utilité de la boucle do-while, la variable est initialisée dans la boucle. On doit tester sa valeur à la fin de l'itération. Tant que l'utilisateur n'aura pas tapé la lettre o, le programme redemandera de taper la lettre.

Exemple 3

http://fr.wikibooks.org/wiki/Discussion:Programmation_C/Pointeurs#Exemple_Complet_sur_les_boucles.2C_les_tableaux_et_les_pointeurs

Arrêt et continuation des boucles

Il arrive fréquemment qu'en évaluant un test à l'intérieur d'une boucle, on aimerait arrêter brutalement la boucle ou alors « sauter » certains cas non significatifs. C'est le rôle des instructions break et continue.

  • break permet de sortir immédiatement d'une boucle for, while ou do-while. À noter que si la boucle se trouve elle-même dans une autre boucle, seule la boucle où l'instruction break se trouvait est stoppée.
  • continue permet de recommencer la boucle depuis le début du bloc. Dans le cas de la boucle for, le bloc lié à l'incrémentation sera exécuté, puis dans tous les cas, la condition sera testée.

Saut inconditionnel (goto)

Cette instruction permet de continuer l'exécution du programme à un autre endroit, dans la même fonction. On l'utilise de la manière suivante :

goto label;

Où label est un identificateur quelconque. Cet identificateur devra être défini quelque part dans la même fonction, avec la syntaxe suivante :

label:

Ce label peut être mis à n'importe quel endroit dans la fonction, y compris avant ou après le goto, ou dans un autre bloc que là où sont utilisés la ou les instructions goto pointant vers ce label. Les labels doivent être uniques au sein d'une même fonction, mais on peut les réutiliser dans une autre fonction.

Un cas où l'emploi d'une instruction goto est tout à fait justifié, par exemple, est pour simuler le déclenchement d'une exception, pour sortir rapidement de plusieurs boucles imbriquées.

void boucle(void)
{
    while (1)
    {
        struct Evenement_t * event;

        attend_evenement();

        while ((event = retire_evenement()))
        {
            switch (event->type) {
            case FIN_TRAITEMENT:
                goto fin;
            /* ... */
            }
        }
    }
    fin:
    /* ... */
}

Dans ce cas de figure, il serait possible d'utiliser aussi un booléen qui indique que le traitement est terminé, pour forcer la sortie des boucles. L'utilisation de goto dans cet exemple ne nuisant pas à la lisibilité du programme et étant tout aussi efficace, il est tout à fait légitime de l'utiliser.

Il faut cependant noter que cette instruction est souvent considérée comme à éviter. En effet, elle provoque un saut dans le code, qui se « voit » beaucoup moins bien que les structures de contrôle comme if ou while, par exemple. De fait, la pratique a montré qu'un abus de son utilisation rend un code source rapidement peu compréhensible, et difficilement maintenable. L'article de Edsger W. Dijsktra Go to considered harmful anglais, datant de 1968, est l'exemple le plus connu de cette critique, qui a donné naissance à la programmation structurée.


Fonctions et procédures

Définition

Le code suivant définit une fonction fonction renvoyant une valeur de type type_retour et prenant N arguments, par1 de type type1, par2 de type type2, etc.

type_retour fonction(type1 par1, type2 par2, /* ..., */ typeN parN)
{
        /* Déclarations de variables ... */
        /* Instructions ... */
}

L'exécution d'une fonction se termine soit lorsque l'accolade fermante est atteinte, soit lorsque le mot clef return est rencontré. La valeur renvoyée par une fonction est donnée comme paramètre à return. Une procédure est une fonction renvoyant void, dans ce cas return est appelé sans paramètre.

Les passages des arguments aux fonctions se font toujours par valeur. Si on veut modifier la valeur d'un argument passé en paramètre à une fonction, en dehors de cette même fonction, il faut utiliser des pointeurs.

Déclaration par prototype

Le prototype d'une fonction correspond simplement à son en-tête (tout ce qui précède la première accolade ouvrante). C'est-à-dire son nom, son type de retour et les types des différents paramètres. Cela permet au compilateur de vérifier que la fonction est appelée avec le bon nombre de paramètres et surtout avec les bons types. La ligne suivante déclare la fonction fonction, mais sans la définir :

type_retour nom_fonction(type1, type2, /* ..., */ typeN);

À noter que les noms des paramètres peuvent être omis et que la déclaration doit se terminer par un point-virgule (;), sans quoi vous pourrez vous attendre à une cascade d'erreurs.

Absence des paramètres

Avant la normalisation par l'ANSI, il était possible de faire une déclaration partielle d'une fonction, en spécifiant son type de retour, mais pas ses paramètres:

int f();

Cette déclaration ne dit rien sur les éventuels paramètes de la fonction f, sur leur nombre ou leur type, au contraire de :

int g(void);

qui précise que la fonction g ne prend aucun argument.

Cette déclaration partielle laissait au compilateur le soin de compléter la déclaration lors de l'appel de la fonction, ou de sa définition. On perd donc un grand intérêt des prototypes. Mais à cause de l'immense quantité de code existant qui se reposait sur ce comportement, l'ANSI (puis le WG14) n'ont pas interdit de tels programmes, mais ont déclaré dès le C90 que cette construction est obsolète.

Évaluation des arguments

La norme du langage ne spécifie pas l'ordre d'évaluation des arguments. Il faut donc faire particulièrement attention aux effets de bords.

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>

int somme(int a, int b)
{return a + b;
}

int main(void)
{
    int i = 0;
    printf("%d\n",  somme(++i, i)  );
    return 0;
}

Voici un premier exemple. Lors de l'appel de la fonction somme, si l'expression ++i est évaluée avant l'expression i, alors le programme affichera 2. Si, au contraire, c'est l'expression i qui est évaluée avant l'expression ++i, alors le programme affichera 1.

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    return fonction(g(), h());
}

Dans cet autre exemple, les expressions g() et h() pouvant être évaluées dans n'importe quel ordre, on ne peut pas savoir laquelle des fonctions g et h sera appelée en premier. Si l'appel de ces fonctions provoque des effets de bord (affichage de messages, modification de variables globales...), alors le comportement du programme est imprévisible. Pour pallier à ce problème, il faut imposer l'ordre d'appel :

#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    int a,b;
    a = g();
    b = h();
    return fonction(a, b);
}

Nombre variable d'arguments

Une fonctionnalité assez utile est d'avoir une fonction avec un nombre variable d'arguments, comme la fameuse fonction printf(). Pour cela, il suffit de déclarer le prototype de la fonction de la manière suivante :

Déclaration

#include <stdarg.h>

void ma_fonction(type1 arg1, type2 arg2, ...)
{
}

Dans l'exemple ci-dessus, les points de suspension ne sont pas un abus d'écriture, mais bel et bien une notation C pour indiquer que la fonction accepte d'autres arguments. L'exemple est limité à deux arguments, mais il est bien sûr possible d'en spécifier autant qu'on veut. C'est dans l'unique but de ne pas rendre ambigüe la déclaration, qu'aucun abus d'écriture n'a été employé.

L'inclusion de l'en-tête <stdarg.h> n'est nécessaire que pour traiter les arguments à l'intérieur de la fonction. La première remarque que l'on peut faire est qu'une fonction à nombre variable d'arguments contient au moins un paramètre fixe. En effet la déclaration suivante est invalide :

Avertissement Ce code contient une erreur volontaire !
void ma_fonction(...);

Accès aux arguments

Pour accéder aux arguments situés après le dernier argument fixe, il faut utiliser certaines fonctions (ou plutôt macros) de l'en-tête <stdarg.h> :

void va_start (va_list ap, last);
type va_arg (va_list ap, type);
void va_end (va_list ap);

va_list est un type opaque dont on n'a pas à se soucier. On commence par l'initialiser avec va_start. Le paramètre last doit correspondre au nom du dernier argument fixe de la fonction, ou alors tout bon compilateur retournera au moins un avertissement.

Vient ensuite la collecte minutieuse des arguments. Il faut bien comprendre qu'à ce stade, le langage n'offre aucun moyen de savoir comment sont structurées les données (c'est à dire leur type). Il faut absolument définir une convention, laissée à l'imagination du programmeur, pour pouvoir extraire les données correctement.

Qui plus est, il faut être extrêmement vigilant lors de la récupération des paramètres, à cause de la promotion des types entiers ou réels. En effet, les entiers sont systématiquement promus en int, sauf si la taille du type est plus grande, auquel cas le type est inchangé. Pour les réels, le type float est promu en double, alors que le type long double est inchangé. C'est pourquoi ce genre d'instruction n'a aucun sens dans une fonction à nombre variable d'arguments :

Avertissement Ce code contient une erreur volontaire !
char caractere = va_arg(list, char);

Il faut obligatoirement récupérer un entier de type char, comme étant un entier de type int.

Exemple de convention

Un bon exemple de convention est la fonction printf() elle même. Elle utilise un spécificateur de format qui renseigne à la fois le nombre d'arguments qu'on s'attend à trouver mais aussi le type de chacun. D'un autre coté, analyser un spécificateur de format est relativement rébarbatif, et on n'a pas toujours besoin d'une artillerie aussi lourde.

Une autre façon de faire, relativement répandue, est de ne passer que des couples (type, objet), où type correspond à un code représentant un type (une énumération par exemple) et objet le contenu de l'objet lui-même (int, pointeur, double, etc.). On utilise alors un code spécial (généralement 0) pour indiquer la fin des arguments, ou alors un des paramètres pour indiquer combien il y en a. Un petit exemple complet :

#include <stdio.h>
#include <stdarg.h>

enum va_list{
        TYPE_FIN, TYPE_ENTIER, TYPE_REEL, TYPE_CHAINE
};

void affiche(FILE * out, ...)
{
        va_list list;
        int     type;

        va_start(list, out);

        while ((type = va_arg(list, int)))
        {
                switch (type)
                {
                case TYPE_ENTIER: fprintf(out, "%d", va_arg(list, int)); break;
                case TYPE_REEL:   fprintf(out, "%g", va_arg(list, double)); break;
                case TYPE_CHAINE: fprintf(out, "%s", va_arg(list, char *)); break;
                }
        }
        fprintf(out, "\n");
        va_end(list);
}

int main(int nb, char * argv[])
{
        affiche(stdout, TYPE_CHAINE, "Le code ascii de 'a' est ", TYPE_ENTIER, 'a', 0);
        affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, 2. * 3 / 5, 0);

        return 0;
}

L'inconvénient de ce genre d'approche est de ne pas oublier le marqueur de fin. Dans les deux cas, il faut être vigilant avec les conversions implicites, notamment dans le second cas. À noter que la conversion (transtypage) explicite des types en une taille inférieure à celle par défaut (int pour les entiers ou double pour les réels) ne permet pas de contourner la promotion implicite. Même écrit de la sorte:

affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, (float) (2. * 3 / 5), 0);

Le résultat transmis au cinquième paramètre sera quand même promu implicitement en type double.

Fonction inline

Il s'agit d'une extension ISO C99, qui à l'origine vient du C++. Ce mot clé doit se placer avant le type de retour de la fonction. Il ne s'agit que d'une indication, le compilateur peut ne pas honorer la demande, notamment si la fonction est récursive. Dans une certaine mesure, les fonctionnalités proposées par ce mot clé sont déjà prises en charge par les instructions du préprocesseur. Beaucoup préfèreront passer par une macro, essentiellement pour des raisons de compatibilité avec d'anciens compilateurs ne supportant pas ce mot clé, et quand bien même l'utilisation de macro est souvent très délicat.

Le mot clé inline permet de s'affranchir des nombreux défauts des macros, et de réellement les utiliser comme une fonction normale, c'est à dire surtout sans effets de bord. À noter qu'il est préférable de classifier les fonctions inline de manière statique. Dans le cas contraire, la fonction sera aussi déclarée comme étant accessible de l'extérieur, et donc définie comme une fonction normale.

En la déclarant static inline, un bon compilateur devrait supprimer toute trace de la fonction et seulement la mettre in extenso aux endroits où elle est utilisée. Ceci permettrait à la limite de déclarer la fonction dans un fichier en-tête, bien qu'il s'agisse d'une pratique assez rare et donc à éviter. Exemple de déclaration d'une fonction inline statique :

static inline int max(int a, int b)
{
    return (a > b) ? a : b;
}

La fonction main

Nous allons revenir ici sur la fonction main, présente dans chaque programme. Cette fonction est le point d'entrée du programme. La norme définit deux prototypes, qui sont donc portables:

int main(int argc, char * argv[]) { /* ... */ }
int main(void) { /* ... */ }

Le premier prototype est plus "général" : il permet de récupérer des paramètres au programme. Le deuxième existe pour des raisons de simplicité, quand on ne veut pas traiter ces arguments.

Si ces deux prototypes sont portables, une implémentation peut néanmoins définir un autre prototype pour main, ou spécifier une autre fonction pour le démarrage du programme. Cependant ces cas sont plus rares (et souvent spécifiques à du C embarqué).

Paramètres de ligne de commande

La fonction main prend deux paramètres qui permettent d'accéder aux paramètres passés au programme lors de son appel. Le premier, généralement appelé argc (argument count), est le nombre de paramètres qui ont été passés au programme. Le second, argv (argument vector), est la liste de ces paramètres. Les paramètres sont stockés sous forme de chaîne de caractères, argv est donc un tableau de chaînes de caractères, ou un pointeur sur un pointeur sur char. argc correspond au nombre d'éléments de ce tableau.

La première chaîne de caractères, dont l'adresse est dans argv[0], contient le nom du programme. Le premier paramètre est donc argv[1]. Le dernier élément du tableau, argv[argc], est un pointeur nul.

Valeur de retour

La fonction main retourne toujours une valeur de type entier. L'usage veut qu'on retourne 0 (ou EXIT_SUCCESS) si le programme s'est déroulé correctement, ou EXIT_FAILURE pour indiquer qu'il y a eu une erreur (Les macros EXIT_SUCCESS et EXIT_FAILURE étant définies dans l'en-tête <stdlib.h>). Il est possible par le programme appelant de récupérer ce code de retour, et de l'interpréter comme bon lui semble.

Exemple

Voici un petit programme très simple qui affiche la liste des paramètres passés au programme lorsqu'il est appelé:

#include <stdio.h>

int main(int argc, char * argv[])
{
    int i;

    for (i = 0; i < argc; i++)
        printf("paramètre %i : %s\n", i, argv[i]);

    return 0;
}

On effectue une boucle sur argv à l'aide de argc. Enregistrez-le sous le nom params.c puis compilez-le (cc params.c -o params). Vous pouvez ensuite l'appeler ainsi:

./params hello world ! # sous Unix
params.exe hello world ! # sous MS-DOS ou Windows

Vous devriez voir en sortie quelque chose comme ceci (paramètre 0 varie selon le système d'exploitation):

paramètre 0 : ./params
paramètre 1 : hello
paramètre 2 : world
paramètre 3 : !

Fonctions en C pré-ANSI

Absence de prototype lors de l'appel d'une fonction

Avant la normalisation C89, on pouvait appeler une fonction sans disposer ni de sa définition, ni de sa déclaration. Dans ce cas, la fonction était implicitement déclarée comme retournant le type int, et prenant un nombre indéterminé de paramètres.

/* Aucune déclaration de g() n'est visible. */

void f(void)
{
    g();  /* Déclaration implicite: extern int g() */
}

À cause de la grande quantité de code existant à l'époque qui reposait sur ce comportement, le C90 a conservé cette possibilité. Cependant, elle a été retirée de la norme C99, et est à éviter même lorsqu'on travaille en C90.

En effet, c'est plus qu'une bonne habitude de programmation de s'assurer que chaque fonction utilisée dans un programme ait son prototype déclaré avant qu'elle ne soit définie ou utilisée. C'est d'autant plus indispensable lorsque les fonctions sont définies et utilisées dans des fichiers différents.

Ancienne notation

À titre anecdotique, ceci est la façon « historique » de définir une fonction, avant que le prototypage ne fut utilisé. Cette notation est interdite depuis C90.

type_retour fonction(par1, par2, ..., parN)
type1 par1;
type2 par2;
...
typeN parN;
{
        /* Déclarations de variables ... */
        /* Instructions ... */
}

Au lieu de déclarer les types à l'intérieur même de la fonction, ils sont simplement décrits après la fonction et avant la première accolade ouvrante. À noter que type_retour pouvait être omis, et dans ce cas valait par défaut int.

Fichiers

Exécuter des fichiers

Les primitives sont execl, execlp, execle, exect, execv, execvp[14].

L'exemple ci-dessous lance un fichier test.exe sur un bureau de Windows 7 :

#include <unistd.h>

int main()
{
     int fichier;
     fichier = execl ( "c:\\Users\\public\\Desktop\\test.exe", "test.exe", ".", (char*)0);
}

Renommer et supprimer des fichiers

Utiliser les fonctions rename et remove[15].

Copier des fichiers

Il faut copier le contenu du premier dans le second[16].


Pointeurs

Dans cette section, nous allons présenter un mécanisme permettant de manipuler les adresses, les pointeurs. Un pointeur a pour valeur l'adresse d'un objet C d'un type donné (un pointeur est typé). Ainsi, un pointeur contenant l'adresse d'un entier sera de type pointeur vers entier.

Usage et déclaration

L'opérateur & permet de connaitre l'adresse d'une variable, on dira aussi la référence. Toute déclaration de variable occupe un certain espace dans la mémoire de l'ordinateur. La référence permet de savoir où cet emplacement se trouve. En simplifiant à l'extrême, on peut considérer la mémoire d'un ordinateur comme une gigantesque table d'octets. Quand on déclare une variable de type int, elle sera allouée à un certain emplacement (ou dit autrement : un indice, une adresse ou une référence) dans cette table. Un pointeur permet simplement de stocker une référence, il peut donc être vu comme un nombre allant de 0 à la quantité maximale de mémoire dont dispose votre ordinateur (moins un, pour être exact).

Un pointeur occupera habituellement toujours la même taille (occupera la même place en mémoire), quelque soit l'objet se trouvant à cet emplacement. Il s'agit en général de la plus grande taille directement gérable par le processeur : sur une architecture 32bits, elle sera de 4 octets, sur une architecture 64bits, 8 octets, etc. Le type du pointeur ne sert qu'à renseigner comment sont organisées les données suivant l'adresse référencée par le pointeur. Ce code, par exemple, affiche la référence d'une variable au format hexadécimal :

int i;

printf("%p\n", &i);

Pouvoir récupérer l'adresse n'a d'intérêt que si on peut manipuler l'objet pointé. Pour cela, il est nécessaire de pouvoir déclarer des pointeurs, ou dit autrement un objet pouvant contenir des références. Pour cela on utilise l'étoile (*) entre le type et le nom de la variable pour indiquer qu'il s'agit d'un pointeur :

T * pointeur, * pointeur2, /* ..., */ * pointeurN;

Déclare les variables pointeur, pointeur2, ..., pointeurN de type pointeur vers le type T. À noter la bizarrerie du langage à vouloir associer l'étoile à la variable et non au type, qui oblige à répéter l'étoile pour chaque variable.

/* Ce code contient une déclaration volontairement confuse */
int * pointeur, variable;

Cet exemple de code déclare un pointeur sur un entier de type int et une variable de type int. Dans un vrai programme, il est rarement possible d'utiliser des noms aussi triviaux, aussi il est recommandé de séparer la déclaration des variables de celles des pointeurs (ou d'utiliser l'instruction typedef, qui, elle, permet d'associer l'étoile au type), la lisibilité du programme sera légèrement améliorée.

Il est essentiel de bien comprendre ce qui a été déclaré dans ces exemples. Chaque pointeur peut contenir une référence sur un emplacement de la mémoire (un indice dans notre fameuse table). On peut obtenir une référence (ou un indice) avec l'opérateur & (ou allouer une référence soi-même avec des fonctions dédiées, c.f la section suivante). Cet opérateur transforme donc une variable de type T en un pointeur de type T *. Insistons sur le terme variable, car évidemment des expressions telles que '&23435' ou '&(2 * a / 3.)' n'ont aucun sens, dans la mesure où les constantes et expressions du langage n'occupent aucun emplacement susceptible d'intéresser votre programme.

Ce code, par exemple, affiche la référence d'une variable dans un format défini par l'implémentation (qui peut être hexadécimal, ou une combinaison "segment:offset", par exemple) :

int i;

printf("%p\n", &i);

Il ne faut pas oublier que, comme toutes les variables locales en C, un pointeur est à l'origine non initialisé. Une bonne attitude de programmation est de s'assurer que lorsqu'il ne pointe pas vers un objet valide, sa valeur est mise à zéro (ou NULL, qui est déclaré entre autre dans <stdio.h>).

L'arithmétique des pointeurs

L'arithmétique associée aux pointeurs est sans doute ce qui a valu au C sa réputation « d'assembleur plus compliqué et plus lent que l'assembleur ». On peut très vite construire des expressions incompréhensibles avec les opérateurs disponibles. Dans la mesure du possible, il est conseillé de se limiter à des expressions simples, quitte à les décomposer, car la plupart des compilateurs savent très bien optimiser un code C.

Déréférencement

Il s'agit de l'opération la plus simple sur les pointeurs. Comme son nom l'indique, il s'agit de l'opération réciproque au référencement (&). L'opérateur associé est l'étoile (*), qui est aussi utilisé pour déclarer un type pointeur. Cet opérateur permet donc de transformer un pointeur de type T *, en un objet de type T, les opérations affectant l'objet pointé :

int variable = 10;
int * pointeur = &variable;

*pointeur = 20; /* Positionne 'variable' à 20 */

Ici, pointeur contient une adresse valide, celle de variable ; son déréférencement est donc possible. Par contre, si pointeur était une variable locale non initialisée, son déréférencement provoquerait à coup sûr un arrêt brutal de votre programme.

Vous obtiendrez le même résultat, si pointeur est initialisé à NULL. Cette adresse est invalide et toute tentative de déréférencement se soldera par un arrêt du programme.

Arithmétique de base

L'arithmétique des pointeurs s'apparente à celle des entiers, mais il est important de comprendre la distinction entre ces deux concepts.

Les opérations arithmétiques permises avec les pointeurs sont :

  • Addition / soustraction d'une valeur entière à un pointeur (on avance / recule d'un nombre de cases mémoires égal à la taille du type T) : le résultat est donc un pointeur, de même type que le pointeur de départ. Il faut bien faire attention avec ce genre d'opération à ne pas sortir du bloc mémoire, car le C n'effectuera aucun test pour vous. Considérez l'exemple suivant :
    /* Parcours les éléments d'un tableau */
int tableau[N];
int * p;

for (p = tableau; p < &tableau[N]; p ++)
{
     /* ... */
}

    Normalement un tableau de N cases permet d'être itéré sur les indices allant de 0 à N - 1, inclusivement. L'expression &tableau[N] fait référence la case mémoire non allouée immédiatement après le plus grand indice, donc potentiellement source de problème. Toutefois, par exception pour le premier indice après le plus grand, C garantit que le résultat de l'expression soit bien défini. Bien sûr, il ne faut pas déréférencer ce pointeur.

    À noter qu'à l'issue de la boucle, p pointera sur la N+1ème case du tableau, donc hors de l'espace alloué. Le C autorise tout à fait ce genre de pratique, il faut juste faire attention à ne pas déréférencer le pointeur à cet endroit.

  • Soustraction de deux pointeurs de même type (combien d'objet de type T y a t-il entre les deux pointeurs) : le résultat est donc un entier, de type ptrdiff_t.
    Avertissement Ce code contient une erreur volontaire !
    int       autre_tableau[3];
int       tableau[10];
int *     p     = &tableau[5];  /* p pointe sur le 6e élément du tableau */
int *     q     = &tableau[3];  /* q pointe sur le 4e élément du tableau */
ptrdiff_t diff1 = p - q;        /* diff1 vaut 2 */
ptrdiff_t diff2 = q - p;        /* diff2 vaut -2 */

q = &autre_tableau[2];
ptrdiff_t dif3 = p - q; /* Erreur ! */

    Dans cet exemple, les deux premières soustractions sont définies, car p et q pointent sur des éléments du même tableau. La troisième soustraction est indéfinie, car on utilise des adresses d'éléments de tableaux différents.

    Notons que l'opérateur [] s'applique toujours à une opérande de type entier et une autre de type pointeur. Lorsqu'on écrit tableau[i], il y a en fait une conversion de tableau à pointeur avec l'application de l'opérateur []. On peut donc bien sûr utiliser l'opérateur [] avec un pointeur pour opérande :

    int   a;
int b;
int * p = &a; /* On peut accéder à la valeur de 'a' via 'p[0]' ou '*p' */

/* p[1] est indéfini - n'espérez pas accéder à la valeur de b depuis l'adresse de a */

Arithmétique avec effet de bord

C'est sans doute ce qui a donné des sueurs froides à des générations de programmeurs découvrant le C : un usage « optimisé » de la priorité des opérateurs, le tout imbriqué dans des expressions à rallonge. Par exemple 'while( *d++ = *s++ );', pour copier une chaine de caractères.

En fait, en décomposant l'instruction, c'est nettement plus simple qu'il ne parait. Par exemple :

int i;
int * entier;

/* ... */

i = *entier++; /* i = *(entier++); */

Dans ce cas de figure, l'opérateur d'incrémentation ayant priorité sur celui de déréférencement, c'est celui-ci qui sera appliqué en premier. Comme il est postfixé, l'opérateur ne prendra effet qu'à la fin de l'expression (donc de l'affectation). La variable i sera donc tout simplement affectée de la valeur pointée par entier et après cela le pointeur sera incrémenté. Voici les différents effets suivant les combinaisons de ces deux opérateurs :

i = *++entier; /* Incrémente d'abord le pointeur, puis déréférence la nouvelle adresse pointée */
i = ++*entier; /* Incrémente la valeur pointée par "entier", puis affecte le résultat à "i" */
i = (*entier)++; /* Affecte la valeur pointée par "entier" et incrémente cette valeur */

On peut évidemment complexifier les expressions à outrance, mais privilégier la compacité au détriment de la clarté et de la simplicité dans un hypothétique espoir d'optimisation est une erreur de débutant à éviter.

Le pointeur void *

Ce pointeur est un cas particulier. Il permet de pointer sur un type quelconque. Il est notemment utilisé dans la fonction malloc():

void * malloc(int n);

En pratique, il faut penser à transformer ce pointeur pour qu'il devienne utilisable, même si certains compilateurs acceptent de l'utiliser directement:

// Allocation avec conversion
int * p; // Pointeur p sur le type int
p = (int *) malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
*p = 4; // Modification

// Allocation sans conversion
int * p; // Pointeur p sur le type int
p = malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
*p = 4; // Modification

Tableaux dynamiques

Un des intérêts des pointeurs et de l'allocation dynamique est de permettre de décider de la taille d'une variable au moment de l'exécution, comme par exemple pour les tableaux. Ainsi pour allouer un tableau de n entiers (n étant connu à l'exécution), on déclare une variable de type pointeur sur entier à laquelle on alloue une zone mémoire correspondant à n entiers :

int * alloue_tableau(int n, size_t taille)
{
    return malloc(n * taille);
}

/* Ailleurs dans le programme */
int * tableau = alloue_tableau(256, sizeof *tableau);
if (tableau  != NULL)
{
    /* opérations sur le tableau */
    /* ... */
    free( tableau );
}

Cet exemple alloue un tableau de 256 cases. Bien que la variable soit un pointeur, il est dans ce cas permis d'accéder aux cases de 0 à 255, soit entre les adresses &tableau[0] et &tableau[255], incluses.

Tableaux dynamiques à deux dimensions

Tout comme on pouvait allouer des tableaux statiques à plusieurs dimensions, on peut allouer des tableaux dynamiques à plusieurs dimensions. Pour ce faire, on commence là-aussi par déclarer un pointeurs approprié : un pointeur sur des pointeurs (etc.) sur des types. Pour déclarer un tableau dynamique d'entiers à deux dimensions :

int ** matrice;

L'allocation d'un tel objet va se dérouler en plusieurs étapes (une par étoile), on alloue d'abord l'espace pour un tableau de pointeurs vers entier. Ensuite, on alloue pour chacun de ces tableaux l'espace pour un tableau d'entiers. Si on veut une matrice 4x5 :

#define LIGNES 4
#define COLONNES 5
int i;
int ** matrice = malloc(sizeof *matrice * LIGNES);

for (i = 0; i < LIGNES; i++)
{
        matrice[i] = malloc(sizeof **matrice * COLONNES);
}

Il ne faut jamais oublier de libérer la mémoire allouée précédemment. Ainsi, à tout appel de malloc doit correspondre un appel de free Pour libérer l'espace alloué ci-dessus, on procède de manière inverse, en commençant par libérer chacune des lignes du tableau, puis le tableau lui même :

for(i = 0; i < LIGNES; i++)
{
        free(matrice[i]);
}
free(matrice);

Tableaux dynamiques à trois dimensions

Voici maintenant un exemple d'une fonction créant un tableau à trois dimensions avec surtout, et c'est très important, les tests des valeurs de retour des fonctions malloc :

int *** malloc_3d(int nb_tableau, int lignes, int colonnes)
{
    int i;
    int j;

    int***t = malloc(sizeof(*t) * nb_tableau);
    /* première dimension */
    if (t==NULL)
        {
            printf ("Impossible d'initialiser avec malloc\n" );
            exit (-1);
        }

    for (i=0;i< nb_tableau;i++) {
        t[i] = malloc(sizeof(**t) * lignes);
        /* deuxième dimension */
        if (t[i]==NULL) {
            printf ("Impossible d'initialiser avec malloc\n" );
            exit (-1);
        }
        for (j=0;j<lignes;j++) {
            /* troisième dimension */
            t[i][j] = malloc(sizeof(***t) * colonnes);
            if (t[i][j]==NULL)
                {
                    printf ("Impossible d'initialiser avec malloc\n" );
                    exit (-1);
            }
        }
    }
    return t;
}

Utilisation des pointeurs sur des tableaux particuliers

Il est possible avec un pointeur de lire/parcourir les éléments d'une structure. Chaque éléments d'une structure utilise un espace qui permet de calculer des déplacements.

 #include <stdio.h>
 #include <string.h>
 struct s{
  int a;
  int b;
  char *s;
 }s;
 int
 main(void)
 {
  struct s st={1,1,"salut"};
  void *p=&st;
  printf ("%s\n",
            (char *)((void **)p)[2]);
  /*<==OU==>*/
  p += 2*sizeof(int);
  printf ("%s\n",
            (char *)((void **)p)[0]);
  /*<=DE LA MEME MANIERE=>*/
  p -= 2*sizeof(int);
  /*====================================*/
  memset (p,-1,
          2*sizeof(int));
  printf ("%i,%i\n",
           st.a, st.b);
  memset ((int *)p,0,
          2*sizeof(int));
  printf ("%i,%i\n",
           st.a, st.b);
  ((int *)p)[0] = 1;
  ((int *)p)[1] = 0;
  printf ("%i,%i\n",
           st.a, st.b);
  printf ("%s\n",st.s);
  return 0;
 }

Vous trouverez un exemple complet sur les tableaux et les pointeur ici[1]

Utilisation des pointeurs pour passer des paramètres par adresse

Toutes les variables en C, à l'exception des tableaux, sont passés par valeurs aux paramètres des fonctions. C'est à dire qu'une copie est effectuée sur la pile d'appel. Si bien que toutes les modifications de la variable effectuées dans la fonction seront perdues une fois de retour à l'appelant. Or, il y a des cas où l'on aimerait bien pouvoir modifier une variable passée en paramètre et que ces modifications perdurent dans la fonction appelante. C'est un des usages des paramètres par adresse : permettre la modification d'une variable de l'appelant, comme dans l'exemple suivant :

#include <stdio.h>
/* Ce code échange le contenu de deux variables */
void echange(int * a, int *b)
{
        int tmp=0,
                  *t[3]={&tmp,a,b};
        unsigned short int i;
        for (i=0;i<3;i++)
             *t[i] = *t[(i!=2)*(i+1)];
        /*tmp = *a;  <=> *t[0]=*t[1] */
        /**a = *b;  <=>  *t[1]=*t[2] */
        /**b = tmp; <=>  *t[2]=*t[0] */
}

int main(void)
{
    int a = 5;
    int b = 2;

    printf("a = %d, b = %d.\n", a, b);

    /* On passe à 'echange' les adresses de a et b. */
    echange(&a, &b);

    printf("a = %d, b = %d.\n", a, b);

    echange(&a, &b);

    printf("a = %d, b = %d.\n", a, b);

    return 0;
}

Ce passage par adresse est extrêmement répandu pour optimiser la quantité de données qui doit transiter sur la pile d'appel (qui est, sur beaucoup de systèmes, de taille fixe). En fait, même si la variable ne doit pas être modifiée, on utilise quand même un passage par adresse, juste pour éviter la copie implicite des variables autres que les tableaux. Ceci est particulièrement intéressant avec les structures, puisque celles-ci ont tendance à être assez imposantes, et cela ne nuit pas trop la lisibilité du programme.

Tableaux dynamiques et passage de tableaux comme arguments d'une fonction

Comme on vient de le voir, l'intérêt principal d'une allocation dynamique est de pouvoir lancer son programme sans connaître la taille du tableau qu'on utilisera; celle-ci sera établie en cours de fonctionnement. Cependant, un autre intérêt du tableau dynamique est de pouvoir être passé comme argument d'une fonction pour des tableaux à plusieurs dimensions.

En reprenant la fonction malloc_3d() vue précédemment, on peut écrire :

int fonction_3d (int ***tab);

int main(int argc, char **argv)
{
   /* Tableau dynamique créé avec des malloc() */
   int ***tab = malloc_3d(2, 10, 10);

   fonction_3d (tab);

    return EXIT_SUCCESS;
}
/* fonction recevant le tableau en 3d */
void fonction_3d (int ***tab)
{
  /* On peut utiliser ici la notation tab[i][j][k] en veillant à
     ce que i, j, k ne sortent pas des bornes du tableau -> dans
     cet exemple tab[3][9][9] est illégal alors que tab[1][3][7]
     peut être utilisé.
     On ne pourra pas savoir si on sort du tableau !
  */
}

Pointeurs vers fonctions

Les pointeurs vers les fonctions sont un peu spéciaux, parce qu'ils n'ont pas d'arithmétique associée (car une telle arithmétique n'aurait pas beaucoup de sens). Les opérations permises avec les pointeurs sur fonctions sont en fait relativement limitées :

type_retour (*pointeur_fonction)(liste_paramètres);

Déclare pointeur_fonction, un pointeur vers une fonction prenant liste_paramètres comme paramètres et renvoyant type_retour. Le parenthésage est ici obligatoire, sans quoi l'étoile se rattacherait au type de retour. Pour faire pointer un pointeur vers une fonction, on utilise une affectation « normale » :

pointeur_fonction = &fonction;
/* Qui est en fait équivalent à : */
pointeur_fonction = fonction;

Où fonction est compatible avec le pointeur (mêmes paramètres et valeur de retour). Une fois que le pointeur pointe vers une fonction, on peut appeler cette fonction :

(*pointeur_fonction)(paramètres);
/* Ou plus simplement, mais moins logique syntaxiquement */
pointeur_fonction(paramètres);

Exemple simple d'utilisation de pointeur de fonction avec retour

On fait une comparaison de deux entiers (5 et 4) via un pointeur sur une fonction de comparaison.

#include <stdio.h>
#include <stdlib.h>

int comparaison_plus_grand_que(int a, int b)
{
    return a > b ;
}

int main(int argc, char * argv[])
{
    int taille = 10;
    int (*p_comparaison)(int,int); // pointeur de fonction
    int a = 5, b = 4;

    p_comparaison = comparaison_plus_grand_que;
    if((*p_comparaison)(a,b)) // appel de la fonction via le pointeur
        printf("%d est plus grand que %d\n", a ,b);
    else
        printf("%d est plus petit que %d\n", a ,b);

    return EXIT_SUCCESS;
}

Pointeurs de fonctions

Exemple sans pointeur
On écrit une fonction graphique.

Cette fonction dessine f.

//INITIALISATION :
f est une FORME GRAPHIQUE
f := "_/-\_"
//.
//DECLARATION DE LA FONCTION dessine_f :
fonction dessine_f()
 dessine f
fin de fonction

Cela donne le résultat :

_/-\_

Avec la fonction f qui est fixe, dessine_f() ne sait dessiner que la forme "_/-\_".

Si on remplace la valeur intrinsèque de f par un pointeur de fonction, la fonction pourra dessiner n'importe quelle forme.

Même exemple en utilisant un pointeur de fonction
En C, le nom d'une fonction est un pointeur. On peut l'utiliser comme argument :
//INITIALISATION :
g, h sont des FORMES GRAPHIQUES
g := "/-\/-\"
h := "--\/--"
//.
//DECLARATION DE LA FONCTION dessine_f :
fonction dessine_f(P_f)
 dessine P_f()
fin de fonction
//.
dessine_f(g)  donnera :"/-\/-\"
dessine_f(h)  donnera :"--\/--"

Voir Exemple graphique (avec Gnuplot) ci-dessous.

Exemple numérique

  • Testé sous Code Block (Windows,Linux).
  • Passer deux pointeurs de fonctions à une fonction.

Code source

  • Ici on passe les deux fonctions f et g à la fonction f1_o_f2().
  • La même fonction peut calculer gof, fog et fof...
  • On peut remarquer que les pointeurs de fonctions ont les mêmes types arguments que les fonctions qu'ils vont recevoir.

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* ------ Fonction f ------------ */
double f(double x){return( pow(x,2.));}
/* ------------------------------ */
char  feq[] = "x**2";
/* ------------------------------ */

/* ------ Fonction g ------------ */
double g(double x){return(2.0*x + 3.0);}
/* ------------------------------ */
char  geq[] = "2.0*x + 3.0";
/* ------------------------------ */

/* -Fonction fog  (g suivie de f)-*/
double f1_o_f2(
double (*P_f1)(double x),/* Pointeur pour la première fonction */
double (*P_f2)(double x),/* Pointeur pour la deuxième fonction */
double a
)
{
 return((*P_f1)( ((*P_f2)(a))) );
}
/* ------------------------------ */

/* ------------------------------ */
int main(void)
{
double a = 2.0;

 printf(" f : x-> %s\n", feq);
 printf(" g : x-> %s\n", geq);
 printf(" \n\n");

   printf(" f(g(%.0f)) = %6.1f\n", a, f1_o_f2(f,g,a));
   printf(" g(f(%.0f)) = %6.1f\n", a, f1_o_f2(g,f,a));
   printf(" f(f(%.0f)) = %6.1f\n", a, f1_o_f2(f,f,a));

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Résultat ;

f : x-> x**2
g : x-> 2.0*x + 3.0

f(g(2)) = 49.0
g(f(2)) = 11.0
f(f(2)) = 16.0

Press return to continue.

Exemple graphique (avec Gnuplot)

  • Testé sous Code Block (Windows,Linux).
  • Passer un pointeurs de fonctions à une fonction.
  • Vous pourrez approfondir ce travail en analyse avec le cours de Wikiversité: Mathc Home Edition.

Code source

  • La fonction Gplt() dessine f(x) et g(x)...
  • On peut remarquer que les pointeurs de fonctions ont les mêmes types arguments que les fonctions qu'ils vont recevoir.
/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* --- Dessinons f et g --------- */
double f(double x){return(  pow(x,2.));}
double g(double x){return(2.0*x + 3.0);}
/* ------------------------------ */

/* Le fichier de points: [a,f(a)] */
void Gplt(
double (*P_f)(double x)
)
{
FILE   *fp;
double   a;

        fp = fopen("data","w");
   for(a = -5.0; a <= 5.0; a += 0.3)
       fprintf(fp," %6.3f   %6.3f\n",
                    a,      ((*P_f)(a)) );
 fclose(fp);
}

/* ------------------------------ */
int main(void)
{

 printf("f) Dans gnuplot -> plot \"data\" ");
    Gplt(f);
 getchar();

 printf("g) Dans gnuplot -> plot \"data\" ");
    Gplt(g);

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Tableau de pointeurs de fonctions

Premier exemple

Nous avons des fonctions semblables. Nous voulons les associer pour pouvoir les manipuler dans des boucles.

Nous allons créer un tableau de pointeurs de fonctions.

Je vais utiliser les fonctions trigonométriques prédéfinies pour faciliter la lecture.

Déclaration d'un tableau de pointeurs de fonctions

double (*TrigF[6])(double x) = {cos,sin,tan,atan,asin,acos};
  • Toutes les fonctions ont la même forme.
  • double FUNCTION(double)
  • Le tableau à la même forme que les fonctions.
  • double ARRAY(double)
  • Il y a six fonctions. (0,1,2,3,4,5)= {cos,sin,tan,atan,asin,acos}.

Exemple d'un appel

   cos(.5) = TrigF[0](.5)

Exemple à tester

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
double (*TrigF[6])(double x) = {cos,sin,tan,atan,asin,acos};

double x= .5;
int    i=  0;

 printf(" Nous avons declare un tableau "\
        " de pointeurs de fonctions.\n  "\
        " J'ai utilise ici les fonctions predefinie du c.\n");

  printf("       cos(%.1f)  = %.3f  \n",  x,     cos(x));
  printf(" TrigF[%d](%.1f)) = %.3f\n\n",i,x,TrigF[i](x));

 printf(" Press return to continue");
 getchar();

 return 0;
}

Application

  • Créer un tableau de valeurs des fonctions trigonométriques.
  • Imprimer le résultat dans cette ordre (sin,cos,tan,acos,asin,atan)
  • Pour .1 <= x <+ .5
  • Ce travail pourrait être récupéré dans une matrice.

Résultat dans un fichier

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
FILE   *fp = fopen("list.txt","w");

double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

int    i= 6;
double x= .1;

fprintf(fp,"   x || sin    cos    tan    acos   asin   atan \n");

     for(;x<=.5;x+=.1)
        {
         fprintf(fp," %.1f ||",x);
         for(i=6;i;)
            fprintf(fp," %.3f ",TrigF[--i](x));
         fprintf(fp,"\n");
        }

 fclose(fp);

 printf("\n\n Ouvrir le fichier list.txt\n");
 getchar();

 return 0;
}

Le résultat :

  x || sin    cos    tan    acos   asin   atan
0.1 || 0.100  0.995  0.100  1.471  0.100  0.100
0.2 || 0.199  0.980  0.203  1.369  0.201  0.197
0.3 || 0.296  0.955  0.309  1.266  0.305  0.291
0.4 || 0.389  0.921  0.423  1.159  0.412  0.381
0.5 || 0.479  0.878  0.546  1.047  0.524  0.464

Remarque :
* Attention à l'ordre des fonctions dans la déclaration du tableau.
* double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

Au démarage :
* La décrémentation ce fait dans le tableau. TrigF[--i](x)
* Il entre 6 dans le tableau.
* 6 est décrémenté -> 5 (avant l'appel de la fonction --i)
* La sixième fonctions est appelé (Sin).
* La numéro cinq. :)

Au final :
* Il entre UN dans le tableau.
* UN est décrémenté -> 0
* La première fonctions est appelé (atan).
* La numéro zéro. :))

* i est égal à zéro en rentrant dans la boucle.
* Le cycle est cassé. :(

Avec résultat à l'écran

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

int main(void)
{
double (*TrigF[6])(double x) = {atan,asin,acos,tan,cos,sin};

int    i= 6;
double x= .1;

     for(;x<=.5;x+=.1)
       {
         printf("\n");
         for(i=6;i;) printf(" %.3f ",TrigF[--i](x));
       }

 printf("\n\n Press return to continue.\n");
 getchar();

 return 0;
}

Deuxième exemple

Nous voulons créer la fonction Derivate capable de calculer la dérivé première et seconde d'une fonction, en utilisant un tableau de pointeurs de fonctions.

Déclaration d'un tableau de pointeurs de fonctions

  • Voir listing en fin de page.
double (*Derivate[3])(double (*P_f)(double x),double a,double h) = {fx,Dx_1,Dx_2};

  • Toutes les fonctions (fx,Dx_1,Dx_2) ont la même forme.
  • double FUNCTION(double (*P_f)(double x) double double)
  • Le tableau à la même forme que les fonctions.
  • double ARRAY(double (*P_f)(double x) double double)

  • Il y a trois fonctions. (0,1,2)= {fx, Dx_1, Dx_2}.

  • La fonction fx donne f.
  • Supprimer cette fonction et travailler sur deux fonctions.
  • Réfléchissez.

Exemple d'un appel

     f(x)=Derivate[0](f,x,0.)

  • Derivate[0] donne f(x).
  • Voir la fonction fx() la première fonction du tableau.
  • h = 0 dans cet appel parce qu'il n'est pas utilisé (voir code de fx())

Exemple à tester

/* ------------------------------ */
#include    <stdio.h>
#include     <math.h>
/* ------------------------------ */

/* ------ Fonction f ------------ */
double f(double x){return( pow(x,2.));}
/* ------------------------------ */
char  feq[] = "x**2";
/* ------------------------------ */

/* ------ Fonction g ------------ */
double g(double x){return(
 pow(cos(x),2.)+sin(x)+x-3);}
/* ------------------------------ */
char  geq[] = "cos(x)**2+sin(x)+x-3";
/* ------------------------------ */

/* ----------------------------- */
double fx(
double (*P_f)(double x),
double a,
double h
)
{
 return( ((*P_f)(a)) );
}

/* ------------------------------
 f'(a) = f(a+h) - f(a-h)
          -------------
              2h
   ------------------------------ */
double Dx_1(
double (*P_f)(double x),
double a,
double h
)
{
 return( ( ((*P_f)(a+h))-((*P_f)(a-h)) ) / (2.*h) );
}

/* -----------------------------
 f''(a) = f(a+h) - 2 f(a) + f(a-h)
           ----------------------
                     h**2
   ------------------------------- */
double Dx_2(
double (*P_f)(double x),
double a,
double h
)
{
 return( (((*P_f)(a+h))-2*((*P_f)(a))+((*P_f)(a-h))) / (h*h) );
}

/* ------------------------------ */
int main(void)
{
double (*Derivate[3])(double (*P_f)(double x),
                      double a,
                      double h) = {fx,Dx_1,Dx_2};
double x = 2;
double h = 0.001;

printf("\n\n");

printf("  f(%.3f) = %.3f = %.3f \n",x,f(x),       Derivate[0](f,x,0.));
printf(" f'(%.3f) = %.3f = %.3f \n",x,Dx_1(f,x,h),Derivate[1](f,x,h));
printf("f''(%.3f) = %.3f = %.3f \n",x,Dx_2(f,x,h),Derivate[2](f,x,h));

printf("\n\n");

printf("  g(%.3f) = %.3f = %.3f \n",x,g(x),       Derivate[0](g,x,0.));
printf(" g'(%.3f) = %.3f = %.3f \n",x,Dx_1(g,x,h),Derivate[1](g,x,h));
printf("g''(%.3f) = %.3f = %.3f \n",x,Dx_2(g,x,h),Derivate[2](g,x,h));

    printf("\n\n Press return to continue.");

 getchar();

 return 0;
}


Tableaux

Il existe deux types de tableaux : les tableaux statiques, dont la taille est connue à la compilation, et les tableaux dynamiques, dont la taille est connue à l'exécution. Nous ne nous intéresserons pour l'instant qu'aux tableaux statiques, les tableaux dynamiques seront présentés avec les pointeurs.

Syntaxe

Déclaration

T tableau[N];

Déclare un tableau de N éléments, les éléments étant de type T. N doit être un nombre connu à la compilation, N ne peut pas être une variable. Pour avoir des tableaux dynamiques, il faut passer par l'allocation mémoire.

Cette dernière restriction a été levée par la norme C99 qui a apporté le type tableau à longueur variable appelés Variable Length Array (VLA). il est maintenant possible de déclarer un tableau de type VLA par :

T tableau[expr];

où expr est une expression entière, calculée à l'exécution du programme. Ce type de tableau ne peut pas appartenir à la classe de stockage static ou extern. Sa portée est limitée au bloc dans lequel il est défini. Par rapport à un allocation dynamique, les risques de fuite mémoire sont supprimés.

Accès aux éléments

L'exemple précédent a permis de déclarer un tableau à N éléments. En C, ces éléments sont indexés de 0 à N-1, et le ie élément peut être accédé de la manière suivante (où i est supposé déclaré comme une variable entière ayant une valeur entre 0 et N-1):

tableau[i]

Note: La syntaxe suivante est équivalente à la précédente:

i[tableau]

Bien qu'autorisée par le C, elle va à l'encontre de ce à quoi nombre de programmeurs sont habitués dans d'autres langages, c'est pourquoi elle est très peu utilisée. Comme, de plus, elle n'apporte aucun réel avantage par rapport à la première syntaxe, elle est simplement à éviter, au profit de la précédente.

Exemples

int i;
int tableau[10]; /* déclare un tableau de 10 entiers */

for (i = 0; i < 10; i++) /* boucle << classique >> pour le parcours d'un tableau */
{
    tableau[i] = i;  /* chaque case du tableau reçoit son indice comme valeur */
}
Avertissement Ce code contient une erreur volontaire !
int tableau[1]; /* déclare un tableau d'un entier */

tableau[10] = 5; /* accède à l'élément d'indice 10 (qui ne devrait pas exister) */

printf("%d\n", tableau[10]);

Ce deuxième exemple, non seulement peut compiler (le compilateur peut ne pas détecter le dépassement de capacité), mais peut aussi s'exécuter et afficher le « bon » résultat. Le langage C n'impose pas à une implémentation de vérifier les accès, en écriture comme en lecture, hors des limites d'un tableau ; il précise explicitement qu'un tel code a un comportement indéfini, donc que n'importe quoi peut se passer. En l’occurrence, ce code peut très bien marcher comme on pourrait l'attendre, i.e. afficher 5, ou causer un arrêt du programme avec erreur (si la zone qui correspondrait au 11ème élément du tableau est hors de la mémoire allouée au processus, le système d'exploitation peut détecter une tentative d'accès invalide à une zone mémoire, ce qui peut se traduire par une « erreur de segmentation » qui termine le programme), ou encore corrompre une autre partie de la mémoire du processus (dans le cas où le pseudo-11ème élément correspondrait à une partie de la mémoire du processus), ce qui peut modifier son comportement ultérieur de manière très difficile à prévoir.

Ce code est donc un exemple d'un type de bogue très courant en langage C. S'il est facile à détecter dans ce code très court, il peut être très difficile à identifier dans du code plus complexe, où on peut par exemple essayer d'accéder à un indice i d'un tableau, la valeur de i pouvant varier suivant un flot d'exécution complexe, ou alors essayer de copier le contenu d'un tableau dans un autre, sans vérifier la taille du tableau de destination (ce dernier cas est connu sous le nom de débordement de tampon, dépassement de capacité, ou encore buffer overflow en anglais). Il est donc très important de s'assurer que tous les accès au contenu de tableaux, en lecture comme en écriture, se font dans les limites de ses bornes.

Tableaux à plusieurs dimensions

Les tableaux vus pour l'instant étaient des tableaux à une dimension (ie : des tableaux à un seul indice), il est possible de déclarer des tableaux possédant un nombre aussi grand que l'on veut de dimensions. Par exemple pour déclarer un tableau d'entiers à deux dimensions :

int matrice[10][5];

Pour accèder aux éléments d'un tel tableau, on utilise une notation similaire à celle vue pour les tableaux à une dimension :

matrice[0][0] = 5;

affecte la valeur 5 à la case d'indice (0,0) du tableau.

Avec le type VLA (Variable Length Array), introduit par C99, il est possible de définir un tableau à plusieurs dimensions dont les bornes ne sont connues qu'à l'exécution et non à la compilation :

static void vlaDemo(int taille1, int taille2)
{
    // Declaration du tableau VLA
    int table[taille1][taille2];

    // ...

Initialisation des tableaux

Il est possible d'initialiser directement les tableaux lors de leur déclaration :

int tableau[5] = { 1 , 5 , 45 , 3 , 9 };

initialise le tableau d'entiers avec les valeurs fournies entre accolades (tableau[0] = 1;, tableau[1] = 5;, etc.)

À noter que si on ne spécifie aucune taille entre les crochets du tableau, le compilateur la calculera automatiquement pour contenir tous les éléments. La déclaration ci-dessus aurait pu s'écrire plus simplement :

int tableau[] = { 1 , 5 , 45 , 3 , 9 };

Si on déclare un tableau de taille fixe, mais qu'on l'initialise avec moins d'éléments qu'il peut contenir, les éléments restant seront mis à zéro. On utilise cette astuce pour initialiser rapidement un tableau à zéro :

int tableau[512] = {0};

Cette technique est néanmoins à éviter, car dans ce cas le tableau sera stocké en entier dans le code du programme, faisant grossir artificiellement la taille du programme exécutable. Alors qu'en ne déclarant qu'une taille et l'initialisant à la main au début du programme, la plupart des formats d'exécutable sont capables d'optimiser en ne stockant que la taille du tableau dans le programme final.

Néanmoins, cette syntaxe est aussi utilisable pour les tableaux à plusieurs dimensions :

int matrice[2][3] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } };

Ce qui peut aussi s'écrire :

int matrice[2][3] = { 1 , 2 , 3 , 4 , 5 , 6 };

À noter que si on veut aussi utiliser l'adaptation dynamique, il faut quand même spécifier une dimension :

int identite[][3] = { { 1 , 0 , 0 } , { 0 , 1 , 0 } , { 0 , 0 , 1 } };
/* Ou plus "simplement" */
int identite[][3] = { { 1 } , { 0 , 1 } , { 0 , 0 , 1 } };

Conversion des noms de tableaux en pointeurs

A quelques exceptions près (c.f. ci-dessous), un nom de tableau apparaissant dans une expression C sera automatiquement converti en un pointeur vers son premier élément lors de l'évaluation de cette expression : si tab est le nom d'un tableau d'entiers, le nom de tableau tab sera converti dans le type pointeur vers int dans l'expression tab, et la valeur de cette expression sera l'adresse du début de la zone mémoire allouée pour le stockage des éléments du tableau, i.e. l'adresse de son premier élément. Le code suivant est par exemple valide :

int tab[10];
int *p, *q;
p = tab;
q = &(tab[0]);

La déclaration int tab[10] réserve une zone mémoire suffisante pour stocker 10 variables de type int, désignées par tab[0], tab[1]..., tab[9]. Dans l'instruction p = tab, l'expression tab est de type pointeur vers int, et la valeur affectée à p est l'adresse de la première case du tableau. Cette adresse est aussi l'adresse de la variable tab[0] : dans la seconde instruction, l'adresse de tab[0] est affectée à q via l'opérateur de prise d'adresse &. Après la seconde instruction, p et q ont la même valeur, et les instructions *p = 1, *q = 1 ou tab[0] = 1 deviennent opératoirement équivalentes.

Exceptions à la règle de conversion

Noter que dans la déclaration int tab[10], le nom tab ne désigne pas en lui-même un pointeur mais bien un tableau, de type tableau d'entiers à 10 éléments, même si ce nom est converti en pointeur dans la plupart des contextes. Les exceptions à la règle de conversion interviennent :

  • lorsque le nom du tableau est opérande de l'opérateur unaire &,
  • lorsqu'il est argument de l'opérateur sizeof,
  • lorsqu'il est argument d'un pré ou d'une post-incrémentation ou décrémentation (--,++)
  • lorsqu'il constitue la partie gauche d'une affectation,
  • lorsqu'il est suivi de l'opérateur d'accès à un champ.

Dans chaque cas, le nom du tableau gardera le sens d'une variable de type tableau, c'est-à-dire conservera son type initial :

  • dans l'expression &tab, la sous-expression tab conserve son type "tableau d'entiers à 10 éléments", et l'expression elle-même est de type "pointeur vers tableaux d'entiers à 10 éléments". Le code suivant est par exemple valide :
int tab[10];  /* tableau d'entiers à 10 éléments */
int (*p)[10]; /* pointeur vers les tableaux d'entiers à 10 éléments */
p = &tab;

Le code suivant est en revanche incorrect :

Avertissement Ce code contient plusieurs erreurs volontaires !
int tab[10];
int *p;
p = &tab; /* incorrect. '&tab' n'est pas du bon type  */
  • dans sizeof tab la taille calculée sera celle de tab considéré encore comme une variable de type "tableau d'entiers à 10 éléments", soit 10 fois la taille d'un int.
  • dans tab++ , tab-- , --tab ou ++tab , la sous-expression tab n'est ni une expression numérique ni un pointeur, mais bien une variable de type tableau : elle n'est donc ni incrémentable, ni décrémentable, et l'expression globale ne sera jamais compilable,
  • dans l'instruction tab = expr où expr est une expression quelconque, tab est toujours considéré comme une variable de type "tableau d'entiers à 10 éléments"... la règle de conversion des noms de tableaux et les restrictions sur les conversion explicites font qu'il est impossible que expr soit de même type : cette affectation générera toujours une erreur de typage, et ne sera jamais compilable,
  • dans l'expression tab.champ où champ est un nom de champ quelconque, tab est de type tableau, i.e. ne possède aucun champ, donc cette expression ne sera jamais compilable.

Le cas où tab est membre gauche d'une affectation montre que les valeurs de type tableau ne sont jamais manipulables de manière directe : il est par exemple impossible d'affecter un tableau à un autre tableau - l'instruction tab_1 = tab_2 est toujours incompilable, à cause de la conversion en pointeur de tab_2 - ou de comparer structurellement deux tableaux par une comparaison simple - dans tab_1 == tab_2 , les deux noms seront convertis en pointeur, et la comparaison effectuée sera celle des adresses.

Cas des paramètres de fonctions

Tout paramètre de fonction déclaré comme étant de type "tableau d’éléments de type T" est automatiquement converti en un pointeur vers T à la compilation. Les deux écritures suivantes sont équivalentes :

version avec tableaux version avec pointeurs
void f(int t[])
{
   /* ... */
}
void g(int m[][10])
{
   /* ... */
}

void f(int *t)
{
   /* ... */
}
void g(int (*m)[10])
{
   /* ... */
}

Dans la première version de f et g , les paramètres t et m sont bien des pointeurs, conformément à l'équivalence avec la seconde version, et sont donc réaffectables dans le corps de ces fonctions. La taille en la première dimension de t et de m , même si elle est spécifiée dans le code (e.g. void g(int m[10][10]) ), sera de toute manière effacée à la compilation : il ne s'agit alors que d'un simple commentaire pour le programmeur.

Cette équivalence est cohérente avec la règle de conversion des noms de tableaux : lors d'un appel de la forme f(tab) où tab est un tableau d'entiers, le nom tab sera converti en pointeur vers int , qui est bien le type du paramètre de f dans la seconde version.

Cas des tableaux externes

Dans le cas des déclarations externes, et contrairement au cas des paramètres de fonctions, il n'y a pas équivalence entre la notation par tableaux et la notation par pointeurs. Le programme suivant est en général compilable, mais incorrect :

Avertissement Ce code contient une erreur volontaire !
fichier1.c fichier2.c
#include <stdio.h>
int tab[] = {42};

void f(void)
{
  printf("fichier1 : tab = %p\n", tab);
}

#include <stdio.h>
extern int *tab;
void f(void)

int main(int nb, char *argv[] )
{
    f();
    printf("fichier2 : tab = %p\n", tab);
    return 0;
}

Les valeurs affichées pour tab dans main et dans f seront en général distinctes. L'erreur se situe dans la déclaration externe de tab située dans le fichier fichier2.c. Il aurait fallu écrire la déclaration suivante, spécifiant correctement le type de tab dans fichier2.c comme étant celui d'un tableau et non d'un pointeur (mais ne spécifiant pas sa taille, le compilateur n'effectuant de toute manière aucun test de dépassement) :

extern int tab[];

L'absence de message d'erreur dans la compilation de la première version de ce programme n'est due qu'à l'insuffisance de la vérification de la cohérence globale du typage lors de la phase de liaison : il est plus généralement possible de déclarer une variable externe de n'importe quel autre type que son type réel, sans que l'incohérence globale du typage soit nécessairement signalée par le compilateur. Aucune norme ne spécifiant le comportement du programme résultant, ce comportement est, de fait, indéfini.

Tableaux VLA passés à une fonction

En C99, il est possible de passer à une fonction un tableau de type VLA (Variable Length Array). Il faut passer les tailles des dimensions en premier.

Exemple de fonction recevant en argument un tableau de type VLA à deux dimensions :

static void vlaPrint(int taille1, int taille2, int table[taille1][taille2])
{
    // Ecriture du tableau sur stdout
    for (int i=0; i< taille1; i++)
    {
        for (int j=0; j< taille2; j++)
        {
            (void)printf("table[%d][%d] = %d\n", i, j, table[i][j]);
        }
    }
}

Chaînes de caractères

Comme il a été dit tout au long de cet ouvrage, les chaînes de caractères sont des tableaux particuliers. En déclarant une chaîne de caractères on peut soit la manipuler en tant que pointeur soit en tant que tableau. Considérez les déclarations suivantes :

char * chaine1 = "Ceci est une chaine";
char chaine2[] = "Ceci est une autre chaine";

Bien que se manipulant exactement de la même façon, les opérations permises sur les deux variables ne sont pas tout à fait les mêmes. Dans le premier cas on déclare un pointeur sur une chaîne de caractères statique, dans le second cas, un tableau (alloué soit sur la pile si la variable est déclarée dans une fonction ou soit dans le segment global si la variable est globale) de taille suffisante pour contenir tous les caractères de la chaîne affectée (incluant le caractère nul).

Le second cas est donc une notation abrégée pour char chaine2[] = { 'C', 'e', 'c', 'i', ' ', ..., 'e', '\0' };. Dans tous les autres cas (ceux où une chaîne de caractères ne sert pas à initialiser un tableau), la chaîne déclarée est statique (les données sont persistantes entre les différents appels de fonctions), et sur certaines architectures, pour ne pas dire toutes, elle est même en lecture seule. En effet l'instruction chaine1[0] = 'c'; peut provoquer un accès illégal à la mémoire. En fait le compilateur peut optimiser la gestion des chaînes en regroupant celles qui sont identiques. C'est pourquoi il est préférable de classifier les pointeurs sur chaîne de caractères avec le mot clé const.

On comprend aisément que si chaine2 est alloué sur la pile (déclaré dans une fonction), la valeur ne pourra pas être utilisée comme valeur de retour. Tandis que la valeur de chaine1 pourra être retournée même si c'est une variable locale.


Types avancés - structures

Types avancés - structures, unions, énumérations

Structures

struct ma_structure {
    type1 champ1;
    type2 champ2;
...
    typeN champN;
} var1, var2, ..., varM;

Déclare une structure (ou enregistrement) ma_structure composé de N champs, champ1 de type type1, champ2 de type type2, etc. On déclare, par la même occasion, M variables de type struct ma_structure.

Accès aux champs

L'accès aux champs d'une structure se fait avec un point :

struct complexe {
    int reel;
    int imaginaire;
} c;

c.reel = 1;
c.imaginaire = 2;

Initialisation

Il y a plusieurs façons d'initialiser une variable de type structure :

  • En initialisant les champs un à un :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable;
variable.ch = 'a';
variable.nb = 12345;
variable.pi = 3.141592;

    Cette façon est néanmoins pénible lorsqu'il y a beaucoup de champs.

  • À la déclaration de la variable :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable = { 'a', 12345, 3.141592 };

    Les valeurs des champs sont assignés dans l'ordre où ils sont déclarés. S'il manque des initialisations, les champs seront initialisés à 0. L'inconvénient, c'est qu'on doit connaitre l'ordre où sont déclarés les champs, ce qui peut être tout aussi pénible à retrouver, et peut causer des plantages lorsque la définition de la structure est modifiée.

  • Une nouveauté du C99 permet d'initialiser certains champs à la déclaration de la variable, en les nommant :
    struct t_mastruct {
    char ch;
    int nb;
    float pi;
};
t_mastruct variable = { .pi = 3.141592, .ch = 'a', .nb = 12345 };

    Les champs non initialisés seront mis à leur valeur par défaut.

Manipulation

La seule opération prise en charge par le langage est la copie, lors des affectations ou des passages de paramètres à des fonctions. Toutes les autres opérations sont à la charge du programmeur, notamment la comparaison d'égalité (cf. section suivante).

Alignement et bourrage (padding)

Il s'agit d'un concept relativement avancé, mais qu'il est bien de connaitre pour agir en connaissance de cause. Lorsqu'on déclare une structure, on pourrait naïvement croire que les champs se suivent les uns à la suite des autres en mémoire. Or, il arrive souvent que des octets soient intercalés entre les champs.

Considérez la structure suivante:

struct ma_structure {
    char  champ1;      /*  8 bits */
    int   champ2;      /* 32 bits */
    char  champ3;      /*  8 bits */
};

On pourrait penser que cette structure occupera 6 octets en mémoire, et pourtant, sur une bonne partie des compilateurs, on obtiendrait une taille plus proche des 12 octets.

En fait, les compilateurs insèrent quasiment toujours des octets entre les champs pour pouvoir les aligner sur des adresses qui correspondent à des mots machines. Cela est dû à une limitation de la plupart des processeurs, qui ne peuvent lire des « mots » de plus d'un octet que s'ils sont alignés sur un certain adressage (alors qu'il n'y a pas de contrainte particulière pour lire un seul octet, si le type char est défini sur 8bit).

En se représentant la mémoire comme un tableau continu, on peut tracer le dessin suivant:

   | bloc N        | bloc N + 1    | bloc N + 2    |
---+---------------+---------------+---------------+---
   | a | b | c | d | e | f | g | h | i | j | k | l |
---+---------------+---------------+---------------+---

Les cases a, b, c, ... représentent des octets, et les blocs des sections de 32 bits. Si on suppose qu'une variable de type ma_structure doive être placée en mémoire à partir du bloc numéro N, alors un compilateur pourra, pour des raisons de performance, placer champ1 en a, champ2 de e à h, et champ3 en i. Cela permettrait en effet d'accéder simplement à champ2: le processeur fournit des instructions permettant de lire ou d'écrire directement le bloc N + 1. Dans ce cas, les octets de b à d ne sont pas utilisés; on dit alors que ce sont des octets de bourrage (ou padding en anglais). Un autre compilateur (ou le même, appelé avec des options différentes) peut aussi placer champ2 de b à e, et champ3 en f, pour optimiser l'utilisation mémoire. Mais alors il devra générer un code plus complexe lors des accès à champ2, le matériel ne lui permettant pas d'accéder en une seule instruction aux 4 octets b à e.

En fait il faut garder à l'esprit que toutes les variables suivent cette contrainte: aussi bien les variables locales aux fonctions, les champs de structures, les paramètres de fonctions, etc.

L'existence d'octets de bourrage ainsi que leur nombre sont non seulement dépendants de l'architecture, mais aussi du compilateur. Cela dit, il est toujours possible de connaître la « distance » (offset) d'un champ par rapport au début de la structure, et ce, de manière portable. Pour cela il existe une macro, déclarée dans l'entête <stddef.h>:

size_t offsetof(type, champ);

La valeur renvoyée est le nombre de char (i.e. d'octets la plupart du temps), entre le début de la structure et celui du champ. Le premier argument attendu est bel et bien le type de la structure et non une variable de ce type, ni un pointeur vers une variable de ce type. Pour s'en souvenir, il suffit de savoir que beaucoup d'implémentations d'offsetof utilisent une arithmétique de ce genre:

size_t distance = (size_t) &((type *)NULL)->champ;

Si type était un pointeur, il faudrait faire un déréférencement supplémentaire (ou éviter l'étoile dans la macro). À noter que, même si cette macro peut s'avérer contraignante (notamment lorsqu'on ne dispose que de type pointeur), il est quand même préférable de l'utiliser pour des raisons de portabilité.

Voici un petit exemple d'utilisation de cette macro:

#include <stddef.h>
#include <stdio.h>

struct ma_structure {
    char  champ1;
    int   champ2;
    char  champ3;
};

int main(void)
{
    /* en C99 */
    /*
    printf("L'offset de 'champ2' vaut %zu.\n",
            offsetof(struct ma_structure, champ2));
    */

    /* en C90 */
    printf("L'offset de 'champ2' vaut %lu.\n",
            (unsigned long) offsetof(struct ma_structure, champ2));
    return 0;
}

Sur une architecture 32 bits, vous obtiendrez très certainement la réponse:

L'offset de 'champ2' vaut 4.

En fait toute cette section était pour souligner le fait qu'il est difficilement portable de comparer les structures comme des blocs binaires (via la fonction memcmp par exemple), car il n'y a aucune garantie que ces octets de bourrage soient initialisés à une certaine valeur. De la même manière, il est sage de prendre quelques précautions avant de transférer cette structure à l'extérieur du programme (comme un fichier, un tube de communication ou une socket IP). En général, il préférable de traiter la structure champ par champ, pour ce genre d'opérations.

Mais comme rien n'est toujours totalement négatif, les conséquences du bourrage offre énormément de souplesse : imaginons 2 structures:

struct str {
    char *string;
};
struct str_ok {
    char *string;
    size_t len;
};

quelle que soit la structure utilisé, on peut avec l'adresse et la cast (struct str *) accéder à la chaine.

int main (void)
{
    struct str s = { "chaine" };
    struct str_ok sok = { "chaine", 0 };
    void *p = &sok;
    if (((struct str *)p)->string)
    {
        sok.len = strlen(sok.string);
        printf("%s\n", ((struct str *)p)->string);
    }
    /* en revanche: Peut-être très dangereux ! */
    p = &s;
    printf("%s\n",((struct str_ok *)p)->string);
    return 0;
}
  1. ↑ Le lecteur intéressé pourra récupérer la dernière version de travail sur la page "projects" du site du WG14.
  2. ↑ Certaines normes sont rendues disponibles gratuitement et en libre téléchargement sur internet, mais leur nombre est relativement faible par rapport à l'ensemble des normes existantes.
  3. ↑ On peut citer comme exemple les commentaires de fin de ligne, les macros à nombre variable d'arguments, les déclarations de symboles à n'importe quel endroit et le mot clé inline pour pallier la précarité des macros
  4. ↑ L'exemple le plus simple serait la définition d'une variable ayant pour identifieur un mot réservé du C++, comme int class;.
  5. ↑ Le lecteur curieux pourra se référer à cet article de David R. Tribble, en anglais, qui énumère les différences entre C99 et C++98.
  6. ↑ La norme C contraint les domaines de valeurs des types signés entre -(2N-1-1) et 2N-1-1, où N est un entier quelconque, et les types non signés entre 0 et 2N-1.
  7. ↑ La liste des types successifs utilisés pour déterminer le type d'une constante entière a changé entre C90 et C99. Cela n'a, quasiment tout le temps, aucune incidence, sauf pour le cas d'une constante de valeur trop grande pour le type long : en C90, le type unsigned long est utilisé, alors qu'en C99 le type long long sera utilisé. Une telle valeur sera alors signée dans un cas, et non signée dans l'autre. Dans ce cas, utiliser un suffixe adapté (UL, LL ou ULL) permet de s'assurer du type de la constante.
  8. ↑ 0,110 = 0.0001100110011...2
  9. ↑ La confusion est surtout née de l'utilisation du mot byte pour désigner la taille du type char dans la norme, alors que ce mot est utilisé dans presque tous les autres domaines de l'informatique pour désigner un octet.
  10. ↑ En occident, en-dehors des codages compatibles avec ASCII, on ne rencontre guère en pratique que la famille de jeux de caractères EBCDIC, utilisée sur des systèmes IBM et quelques mainframes.
  11. ↑ Le site Unicode publie une liste de bibliothèque de gestion des caractères et chaînes Unicodes. http://www.unicode.org/resources/libraries.html
  12. ↑ Ces trois expressions sont définies sous formes de macro pour le préprocesseur.
  13. ↑ La norme C99 a ajouté le type _Bool, mais celui-ci est peu utilisé, l'usage des types entiers pour représenter les booléens en C étant très répandu (voir Booléens).
  14. ↑ http://www.lipn.fr/~cerin/SE/SeETlangC.pdf
  15. ↑ http://www.siteduzero.com/tutoriel-3-14052-lire-et-ecrire-dans-des-fichiers.html#ss_part_4
  16. ↑ http://c.developpez.com/faq/?page=fichiers#FICHIERS_copier

Pointeurs vers structures

Il est (bien entendu) possible de déclarer des variables de type pointeur vers structure :

struct ma_struct * ma_variable;

Comme pour tout pointeur, on doit allouer de la mémoire pour la variable avant de l'utiliser :

ma_variable = malloc( sizeof(struct ma_struct) );

L'accès aux champs peut se faire comme pour une variable de type structure « normale » :

(* ma_variable).champ

Ce cas de figure est en fait tellement fréquent qu'il existe un raccourci pour l'accès aux champs d'un pointeur vers structure :

ma_variable->champ

Unions

Une union et un enregistrement se déclarent de manière identique :

union type_union
{
    type1 champ1;
    type2 champ2;
    /* ... */
    typeN champN;
};

/* Déclaration de variables */
union type_union var1, var2, /* ... */ varM;

Toutefois, à la différence d'un enregistrement, les N champs d'une instance de cette union occupent le même emplacement en mémoire. Modifier l'un des champ modifie donc tous les champs de l'union. Typiquement, une union s'utilise lorsqu'un enregistrement peut occuper plusieurs fonctions bien distinctes et que chaque fonction ne requiert pas l'utilisation de tous les champs.

Voici un exemple ultra classique d'utilisation des unions, qui provient en fait de la gestion des événements du système X-Window, très répandu sur les systèmes Unix :

union _XEvent
{
    int type;

    XAnyEvent xany;
    XKeyEvent xkey;
    XButtonEvent xbutton;
    XMotionEvent xmotion;
    XCrossingEvent xcrossing;
    XFocusChangeEvent xfocus;
    XExposeEvent xexpose;
    /* ... */
    XErrorEvent xerror;
    XKeymapEvent xkeymap;
    long pad[24];
};

La déclaration a juste été un peu raccourcie pour rester lisible. Les types X*Event de l'union sont en fait des structures contenant des champs spécifiques au message. Par exemple, si type vaut ButtonPress, seules les valeurs du champ xbutton seront significatives. Parmi ces champs, il y a button qui permet de savoir quel bouton de la souris à été préssé :

    XEvent ev;
    XNextEvent(display, &ev);

    switch (ev.type) {
    case ButtonPress:
        printf("Le bouton %d a été pressé.\n", ev.xbutton.button);
        break;
    default:
        printf("Message type %d\n", ev.type);
    }

En offrant une souplesse nécessaire à la manipulation de l'objet, la gestion de la mémoire en C peut aider à réduire les erreurs de programmation lié à la structure de la mémoire (erreur de segmentation/segfault/"Argh!!!"):

#include <stdio.h>
#include <string.h>

struct s1 {
    char *string;
} s1;

struct s2 {
    char   *string;
    size_t len;
    int    ______;
    int    setnull;
} s2;

union str {
    struct s2 s_;
    struct s1 _s;
}str;

typedef void * STR_s1;
typedef void * STR_s2;

STR_s2
string_analyse(STR_s1 *s)
{
    static union str us, init_us = {{NULL,0,0,0}};
    memcpy((void *)&us, (void *)&init_us, sizeof(union str));
    if (!((struct s1 *)s)->string)
    {
        return &us._s;
    }
    us.s_.len = strlen(((struct s1 *)s)->string);
    if (!us.s_.len)
    {
        us.s_.setnull = 1;
        return &us._s;
    }
    us.s_.string = ((struct s1 *)s)->string;
    return &us.s_;
}

int main(void)
{
    struct s1 s = {NULL}, s_ = {"salut"}, s__ = {""};
    void *p;
    p = string_analyse((void *)&s);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    p = string_analyse((void *)&s_);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    p = string_analyse((void *)&s__);
    printf("null? %s,%lu,%i\n",
        ((struct s2 *)p)->string, ((struct s2 *)p)->len, ((struct s2 *)p)->setnull);
    printf("Réalisé en toute sécurité...\n"
        "Même pour un petit vaisseau serial killer du monde d'en haut.\n");
    return 0;
}

Tout en permettant l'optimisation:

struct s1{
    size_t len;
    char   *str;
} s1;

union s {
    struct s1  dbl[2];
    void       *perform[5];
    struct s1  simple;
};

void * | void * | void * | void * | void *
size_t | char * | size_t | char * | ---
size_t | char * | ------ |  | ---
n oct | n oct | n oct | n oct | 2int=n oct

ce qui signifie que:

int main(void)
{
    const char *end[2] = {", ", "\n"};
    union s init = {{{5,"Salut"}, {3,"toi"}}}, us;
    void *p;
    memset((void *)&us, 0,40);
    memcpy((void *)&us,
            (void *)&init,
            2 * sizeof(struct s1));
    for (p = &us;
        ((union s *)p)->perform[0] != (void *)0;
        p += 16)
        printf("%s%s",
                (char *)((union s *)p)->perform[1],
                end[
                     !((char *)((union s *)p)->perform[2])]
                );
    return 0;
}

Définitions de synonymes de types (typedef)

Le langage C offre un mécanisme assez pratique pour définir des synonymes de types. Il s'agit du mot-clé typedef.

typedef un_type synonyme_du_type;

Contrairement aux langages à typage fort comme le C++, le C se base sur les types atomiques pour décider de la compatibilité entre deux types. Dit plus simplement, la définition de nouveaux types est plus un mécanisme d'alias qu'une réelle définition de type. Les deux types sont effectivement parfaitement interchangeables. À la limite on pourrait presque avoir les mêmes fonctionnalités en utilisant le préprocesseur C, bien qu'avec ce dernier vous aurez certainement beaucoup de mal à sortir de tous les pièges qui vous seront tendus.

Quelques exemples

typedef unsigned char octet;
typedef double matrice4_4[4][4];
typedef struct ma_structure * ma_struct;
typedef void (*gestionnaire_t)( int );

/* Utilisation */
octet nombre = 255;
matrice4_4 identite = { {1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0,0,1} };
ma_struct pointeur = NULL;
gestionnaire_t fonction = NULL;

Ce mot clé est souvent utilisé conjointement avec la déclaration des structures, pour ne pas devoir écrire à chaque fois le mot clé struct. Elle permet aussi de grandement simplifier les prototypes de fonctions qui prennent des pointeurs sur des fonctions en argument, ou retournent de tels types. Il est conseillé dans de tels cas de définir un type synonyme, plutôt que de l'écrire in extenso dans la déclaration du prototype. Considérez les deux déclarations :

/* Déclaration confuse */
void (*fonction(int, void (*)(int)))(int);

/* Déclaration claire avec typedef */
typedef void (*handler_t)(int);

handler_t fonction(int, handler_t);

Les vétérans auront reconnu le prototype imbitable de l'appel système signal(), qui permet de rediriger les signaux (interruption, alarme périodique, erreur de segmentation, division par zéro, ...).

Énumérations

enum nom_enum { val1, val2, ..., valN };

Les symboles val1, val2, ..., valN pourront être utilisés littéralement dans la suite du programme. Ces symboles sont en fait remplacés par des entiers lors de la compilation. La numérotation commençant par défaut à 0, s'incrémentant à chaque déclaration. Dans l'exemple ci-dessus, val1 vaudrait 0, val2 1 et valN N-1.

On peut changer à tout moment la valeur d'un symbole, en affectant au symbole, la valeur constante voulue (la numérotation recommençant à ce nouvel indice). Par exemple :

enum Booleen { Vrai = 1, Faux = 0 };

/* Pour l'utiliser */
enum Booleen variable = Faux;

Ce qui est assez pénible en fait, puisqu'il faut à chaque fois se souvenir que le type Booleen est dérivé d'une énumération. Il est préférable de simplifier les déclarations, grâce à l'instruction typedef :

typedef enum { Faux, Vrai } Booleen;

/* Pour l'utiliser */
Booleen variable = Faux;

Type incomplet

Pour garantir un certain degré d'encapsulation, il peut être intéressant de masquer le contenu d'un type complexe, pour éviter les usages trop « optimisés » de ce type. Pour cela, le langage C permet de déclarer un type sans indiquer explicitement son contenu.

struct ma_structure;

/* Plus loin dans le code */
struct ma_structure * nouvelle = alloue_objet();

Les différents champs de la structure n'étant pas connus, le compilateur ne saura donc pas combien de mémoire allouer. On ne peut donc utiliser les types incomplets qu'en tant que pointeur. C'est pourquoi, il est pratique d'utiliser l'instruction typedef pour alléger les écritures :

typedef struct ma_structure * ma_struct;

/* Plus loin dans le code */
ma_struct nouvelle = alloue_objet();

Cette construction est relativement simple à comprendre, et proche d'une conception objet. À noter que le nouveau type défini par l'instruction typedef peut très bien avoir le même nom que la structure. C'est juste pour éviter les ambiguités, qu'un nom différent a été choisi dans l'exemple.

Un autre cas de figure relativement classique, où les types incomplets sont très pratiques, ce sont les structures s'auto-référençant, comme les listes chainées, les arbres, etc.

struct liste
{
    struct liste * suivant;
    struct liste * precedant;
    void *         element;
};

Ou de manière encore plus tordue, plusieurs types ayant des références croisées :

struct type_a;
struct type_b;

struct type_a
{
    struct type_a * champ1;
    struct type_b * champ2;
    int             champ3;
};

struct type_b
{
    struct type_a * ref;
    void *          element;
};


Préprocesseur

Le préprocesseur est un langage de macro qui est analysé, comme son nom l'indique, avant la compilation. En fait, c'est un langage complètement indépendant, il est même théoriquement possible de l'utiliser par dessus un autre langage que le C. Cette indépendance fait que le préprocesseur ignore totalement la structure de votre programme, les directives seront toujours évaluées de haut en bas.

Ces directives commencent toutes par le symbole dièse (#), suivi d'un nombre quelconque de blancs (espace ou tabulation), suivi du nom de la directive en minuscule. Les directives doivent être déclarées sur une ligne dédiée. Les noms standards de directives sont :

  • define : définit un motif de substitution.
  • undef : retire un motif de substitution.
  • include : inclusion de fichier.
  • ifdef, ifndef, if, else, elif, endif : compilation conditionnelle.
  • pragma : extension du compilateur.
  • error : émettre un message d'erreur personnalisé et stopper la compilation.

Variables de substitution et macros

Déclarations de constantes

Les variables de substitution du préprocesseur fournissent un moyen simple de nommer des constantes. En effet :

#define CONSTANTE valeur

permet de substituer presque partout dans le code source qui suit cette ligne la suite de caractères « CONSTANTE » par la valeur. Plus précisément, la substitution se fait partout, à l'exception des caractères et des chaines de caractères. Par exemple, dans le code suivant :

#define TAILLE 100

printf("La constante TAILLE vaut %d\n", TAILLE);

La substitution se fera sur la deuxième occurrence de TAILLE, mais pas la première. Le préprocesseur transformera ainsi l'appel à printf :

printf("La constante TAILLE vaut %d\n", 100);

Le préprocesseur procède à des traitements sur le code source, sans avoir de connaissance sur la structure de votre programme. Dans le cas des variables de substitution, il ne sait faire qu'un remplacement de texte, comme le ferait un traitement de texte. On peut ainsi les utiliser pour n'importe quoi, des constantes, des expressions, voire du code plus complexe ; le préprocesseur n'ira pas vérifier si la valeur de remplacement est une expression C ou non. Dans l'exemple précédent, nous avons utilisé TAILLE pour remplacer la constante 100, mais toute suite de caractères peut être définie. Par exemples:

#define PREFIXE "erreur:"
#define FORMULE (1 + 1)

printf(PREFIXE "%d\n", FORMULE);

Ici, l'appel à printf sera transformé ainsi :

printf("erreur:" "%d\n", (1 + 1));

Une définition de constantes peut s'étendre sur plusieurs lignes. Pour cela, il faut que le dernier caractère de la ligne soit une barre oblique inverse ('\'). On peut ainsi définir une macro qui est remplacée par un bout de code plus complexe :

#define BLOC_DEFAUT            \
    default:                   \
        puts("Cas interdit."); \
        break;

Cette variable de substitution permet ainsi d'ajouter le cas par défaut à un switch.

Historiquement, les programmeurs avaient pour habitude d'utiliser des capitales pour distinguer les déclarations du préprocesseur et les minuscules pour les noms de symboles (fonctions, variables, champs, etc.) du compilateur. Ce n'est pas une règle à suivre impérativement, mais elle améliore la lisibilité des programmes.

Il est possible de définir plusieurs fois la même « CONSTANTE ». Le compilateur n'émettra un avertissement que si les deux valeurs ne concordent pas.

En fait, dans la section précédente, il était fait mention d'un mécanisme relativement similaire : les énumérations. On peut légitimement se demander ce que ces énumérations apportent en plus par rapport aux directives du préprocesseur. En fait, on peut essentiellement souligner que :

  • Lors des cas multiples (switch), le compilateur peut vérifier que l'ensemble des cas couvre l'intervalle de valeur du type, et émettre un avertissement si ce n'est pas le cas. Ce qui est évidemment impossible à faire avec des #define.
  • Là où l'utilité est plus flagrante, c'est lors du débogage. Un bon débogueur peut afficher le nom de l'élément énuméré, au lieu de simplement une valeur numérique, ce qui rends un peu moins pénible ce processus déjà très rébarbatif à la base, surtout lorsque le type en question est une structure avec des dizaines, pour ne pas dire une centaine, de champs.
  • Certains compilateurs (gcc pour ne citer que le plus connu) n'incluent pas par défaut les symboles du préprocesseur avec l'option standard de débogage (-g), principalement pour éviter de faire exploser la taille de l'exécutable. Si bien que dans un débogueur il est souvent impossible d'afficher la valeur associée à une constante du préprocesseur autrement qu'en fouillant dans les sources. À moins d'avoir un environnement plutôt évolué, cette limitation peut s'avérer très pénible.

Déclarations automatiques

Le langage C impose que le compilateur définisse un certain nombre de constantes. Sans énumérer toutes celles spécifiques à chaque compilateur, on peut néanmoins compter sur :

  • __FILE__ (char *) : une chaîne de caractères représentant le nom de fichier dans lequel on se trouve. Pratique pour diagnostiquer les erreurs.
  • __LINE__ (int) : le numéro de la ligne en cours dans le fichier.
  • __DATE__ (char *) : la date en cours (incluant le jour, le mois et l'année).
  • __TIME__ (char *) : l'heure courante (HH:MM:SS).
  • __STDC__ (int) : cette constante est en général définie si le compilateur suit les règles du C ANSI (sans les spécificités du compilateur). Cela permet d'encadrer des portions de code non portables et fournir une implémentation moins optimisée, mais ayant plus de chance de compiler sur d'autres systèmes.

Extensions

Chaque compilateur peut définir d'autres macro, pourvu qu'elles restent dans les conventions de nommage que la norme leur réserve. Les lister toutes ici est impossible et hors-sujet, mais on peut citer quelques unes, à titre d'exemple, que le lecteur pourra rencontrer régulièrement dans du code. Il va de soi que le fait qu'elles soient définies ou non, et leur éventuelle valeur, est entièrement dépendant du compilateur.

  • Détection du système d'exploitation :
    • _WIN32 ou __WIN32__ (Windows)
    • linux ou __linux__ (Linux)
    • __APPLE__ ou __MACH__ (Apple Darwin)
    • __FreeBSD__ (FreeBSD)
    • __NetBSD__ (NetBSD)
    • sun ou __SVR4 (Solaris)
  • Visual C++ : _MSC_VER
  • Compilateur gcc :
    • Version : __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__.
  • Compilateurs Borland :
    • __TURBOC__ (version de Turbo C ou Borland C)
    • __BORLANDC__ (version de Borland C++ Builder)
  • Divers : __MINGW32__ (MinGW), __CYGWIN__ ou __CYGWIN32__ (Cygwin),

Déclaration de macros

Une macro est en fait une constante qui peut prendre un certain nombre d'arguments. Les arguments sont placés entre parenthèses après le nom de la macro sans espaces, par exemple :

#define MAX(x,y) x > y ? x : y
#define SWAP(x,y) x ^= y, y ^= x, x ^= y

La première macro prend deux arguments et « retourne » le maximum entre les deux. La deuxième est plus subtile, elle échange la valeur des deux arguments (qui doivent être des variables entières), sans passer par une variable temporaire, et ce avec le même nombre d'opérations.

La macro va en fait remplacer toutes les occurrences de la chaîne « MAX » et de ses arguments par « x > y ? x : y ». Ainsi, si on appelle la macro de cette façon :

printf("%d\n", MAX(4,6));

Elle sera remplacée par :

printf("%d\n", 4 > 6 ? 4 : 6);

Il faut bien comprendre qu'il ne s'agit que d'une substitution de texte, qui ne tient pas compte de la structure du programme. Considérez l'exemple suivant, qui illustre une erreur très classique dans l'utilisation des macros :

#define MAX(x,y) x > y ? x : y

i = 2 * MAX(4,6); /* Sera remplacé par : i = 2 * 4 > 6 ? 4 : 6; */

L'effet n'est pas du tout ce à quoi on s'attendait. Il est donc important de bien parenthéser les expressions, justement pour éviter ce genre d'effet de bord. Il aurait mieux fallu écrire la macro MAX de la manière suivante :

#define MAX(x,y) ((x) > (y) ? (x) : (y))

En fait dès qu'une macro est composée d'autre chose qu'un élément atomique (un lexème, ou un token) il est bon de le mettre entre parenthèses, notamment les arguments qui peuvent être des expressions utilisant des opérateurs ayant des priorités arbitraires.

Il est a noter que l'emploi systématique de parenthèses ne protège pas contre tous les effets de bord. En effet :

MAX(x++,y); /* Sera remplacé par : ((x++) > (y) ? (x++) : (y)) */

Du coup, x est incrémenté de 2 et non pas de 1. La qualité et la performance des compilateurs C modernes fait que l'utilisation de fonctions inline est le plus souvent préférable.

Suppression d'une définition

Il arrive qu'une macro/constante soit déjà définie, mais qu'on aimerait quand même utiliser ce nom avec une autre valeur. Pour éviter un avertissement du préprocesseur, on doit d'abord supprimer l'ancienne définition, puis déclarer la nouvelle :

#undef symbole
#define symbole nouvelle_valeur

Cette directive supprime le symbole spécifié. À noter que même pour les macros avec arguments, il suffit juste de spécifier le nom de cette macro. Qui plus est, supprimer une variable de substitution inexistante ne provoquera aucune erreur.

Transformation en chaîne de caractères

Le préprocesseur permet de transformer une expression en chaîne de caractères. Cette technique ne fonctionne donc qu'avec des macros ayant au moins un argument. Pour transformer n'importe quel argument de la macro en chaîne de caractères, il suffit de préfixer le nom de l'argument par le caractère dièse ('#'). Cela peut être utile pour afficher des messages de diagnostique très précis, comme dans l'exemple suivant :

#define assert(condition) \
if( (condition) == 0 ) \
{ \
        puts( "La condition '" #condition "' a échoué" ); \
        exit( 1 );            \
}

À noter qu'il n'existe pas de mécanisme simple pour transformer la valeur de la macro en chaîne de caractères. C'est le cas classique des constantes numériques qu'on aimerait souvent transformer en chaîne de caractères : le préprocesseur C n'offre malheureusement rien de vraiment pratique pour effectuer ce genre d'opération.

Concaténation d'arguments

Il s'agit d'une facilité d'écriture pour simplifier les tâches répétitives. En utilisant l'opérateur ##, on peut concaténer deux expressions :

#define version(symbole) symbole ## _v123

int version(variable); /* Déclare "int variable_v123;" */

Déclaration de macros à nombre variable d'arguments

Ceci est une nouveauté du C99. La déclaration d'une macro à nombre variable d'arguments est en fait identique à une fonction, sauf qu'avec une macro on ne pourra pas traiter les arguments supplémentaires un à un. Ces paramètres sont en fait traités comme un tout, via le symbole __VA_ARGS__. Exemple :

#define debug(message, ...) fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, __VA_ARGS__ )

Il y a une restriction qui ne saute pas vraiment aux yeux dans cet exemple, c'est que les points de suspension doivent obligatoirement être remplacés par au moins un argument, ce qui n'est pas toujours très pratique. Malheureusement le langage C n'offre aucun moyen pour contourner ce problème pourtant assez répandu.

À noter, une extension du compilateur gcc, qui permet de s'affranchir de cette limitation en rajoutant l'opérateur ## à __VA_ARGS__ :

/* Extension de gcc pour utiliser la macro sans argument */
#define debug(message, ...) fprintf( stderr, __FILE__ ":%d:" message "\n", __LINE__, ##__VA_ARGS__ )

Ces macros peuvent être utilisées par exemple pour des traitements autour de la fonction printf, voir par exemple Variadic Macros anglais.

Exemples

Les macro du préprocesseur sont souvent utilisées dans des situations où une fonction, éventuellement inline, aurait le même effet, avec une meilleure robustesse. Cependant, il y a des usages pour lesquels les macros ne peuvent être remplacés par des fonctions.

Par exemple, pour afficher la taille des types C, on écrirait un programme comme le suivant :

#include <stdio.h>

int main(void)
{
    printf("sizeof(char) = %zd.\n", sizeof(char));
    printf("sizeof(short) = %zd.\n", sizeof(short));
    printf("sizeof(int) = %zd.\n", sizeof(int));
    /* ...*/
    return 0;
}

Écrire un tel programme peut être fatiguant, et il est très facile de faire des erreurs en recopiant les lignes. Ici, une fonction ne pourrait simplifier l'écriture. En effet, il faudrait passer à la fonction le nom du type, pour l'afficher dans la chaîne "sizeof(XXX) = ", et la taille. On devrait donc donner deux arguments à la fonction, pour l'appeler ainsi:

print_taille("char", sizeof(char));

Ce qui ne serait pas beaucoup plus simple, surtout que la fonction elle-même serait plus complexe (utilisation de fonctions str* pour inclure le premier paramètre dans la chaîne de format). Ici, le préprocesseur fournit une solution beaucoup plus simple, à l'aide de l'opérateur # :

#include <stdio.h>

#define PRINTF_SIZEOF(type) printf("sizeof(" #type ") = %zd.\n", sizeof(type))

int main(void)
{
    PRINTF_SIZEOF(char);
    PRINTF_SIZEOF(short);
    PRINTF_SIZEOF(int);
    return 0;
}

Une technique similaire peut être utilisée pour conserver à l'affichage le nom de constantes ou d'énumérations :

#include <stdio.h>
enum etat_t { Arret, Demarrage, Marche, ArretEnCours };

#define CASE_ETAT(etat) case etat: printf("Dans l'état " #etat "\n"); break;

void affiche_etat(etat_t etat)
{
  switch (etat)
  {
     CASE_ETAT(Arret)
     CASE_ETAT(Demarrage)
     CASE_ETAT(Marche)
     CASE_ETAT(ArretEnCours)
     default:
         printf("Etat inconnu (%d).\n", etat);
         break;
  }
}

Ces exemples sont tirés d'un message de Bill Godfrey sur comp.lang.c.

Compilation conditionnelle

Les tests permettent d'effectuer de la compilation conditionnelle. La directive #ifdef permet de savoir si une constante ou une macro est définie. Chaque déclaration #ifdef doit obligatoirement se terminer par un #endif, avec éventuellement une clause #else entre. Un petit exemple classique :

#ifdef DEBUG
/* S'utilise : debug( ("Variable x = %d\n", x) ); (double parenthésage) */
#define debug(x) printf x
#else
#define debug(x)
#endif

L'argument de la directive #ifdef doit obligatoirement être un symbole du préprocesseur. Pour utiliser des expressions un peu plus complexes, il y a la directive #if (et #elif, contraction de else if). Cette directive utilise des expressions semblable à l'instruction if() : si l'expression évaluée est différente de zéro, elle sera considéré comme vraie, et comme fausse si l'expression s'évalue à 0.

On peut utiliser l'addition, la soustraction, la multiplication, la division, les opérateurs binaires (&, |, ^, ~, <<, >>), les comparaisons et les connecteurs logiques (&& et ||). Ces derniers sont évalués en circuit court comme leur équivalent C. Les opérandes possibles sont les nombres entiers et les caractères, ainsi que les macros elle-mêmes, qui seront remplacés par leur valeur.

À noter, l'opérateur spécial defined qui permet de tester si une macro est définie (et qui renvoit donc un booléen). Exemple :

#if defined(DEBUG) && defined(NDEBUG)
#error DEBUG et NDEBUG sont définis !
#endif

Cette directive est très pratique pour désactiver des pans entier de code sans rien y modifier. Il suffit de mettre une expression qui vaudra toujours zéro, comme :

#if 0
/* Vous pouvez mettre ce que vous voulez ici, tout sera ignoré, même du code invalide */
:-)
#endif

Un autre avantage de cette technique, est que les directives de compilations peuvent être imbriquées, contrairement aux commentaires. Dans ce cas, s'il y avait eu d'autres directives #if / #endif (correctement balancées), la clause #if 0 ne s'arrêterait pas au premier #endif rencontré, tandis que cela aurait été le cas avec des commentaires.

À noter qu'au niveau du préprocesseur il est impossible d'utiliser les opérateurs du C. Notamment l'opérateur sizeof, dont le manque aura fait grincer les dents à des générations de programmeurs, sera en fait interprété comme étant une macro. Il faut bien garder à l'esprit que le préprocesseur C est totalement indépendant du langage C.

Inclusion de fichiers

Il s'agit d'une autre fonctionnalité massivement employée dans toute application C qui se respecte. Comme son nom l'indique, la directive #include permet d'inclure in extenso le contenu d'un autre fichier, comme s'il avait été écrit en lieu et place de la directive. On peut donc voir ça comme de la factorisation de code, ou plutôt de déclarations. On appelle de tels fichiers, des fichiers en-têtes (header files) et on leur donne en général l'extension .h.

Il est rare d'inclure du code dans ces fichiers (définitions de variables ou de fonctions), principalement parce que ces fichiers sont destinés à être inclus dans plusieurs endroits du programme. Ces définitions seraient donc définies plusieurs fois, ce qui, dans le meilleur des cas, seraient du code superflu, et dans le pire, pourraient poser des problèmes à l'édition des liens.

On y place surtout des déclarations de type, macros, prototypes de fonctions relatif à un module. On en profite aussi pour documenter toutes les fonctions publiques, leurs paramètres, les valeurs renvoyées, les effets de bords, la signification des champs de structures et les pré/post conditions (quel état doit respecter la fonction avant/après son appel). Dans l'idéal on devrait pouvoir comprendre le fonctionnement d'un module, simplement en lisant son fichier en-tête.

La directive #include prend en fait un argument : le nom du fichier que vous voulez inclure. Ce nom doit être soit mis entre guillemets doubles ou entre balises (< >). Cette différence affecte simplement l'ordre de recherche du fichier. Dans le premier cas, le fichier est recherché dans le répertoire où le fichier contenant la directive se trouve, puis le préprocesseur regarde à des endroits préconfigurés. Dans le second cas, il regardera seulement dans les endroits préconfigurés. Les endroits préconfigurés sont le répertoire include par défaut (par exemple /usr/include/, sous Unix) et ceux passés explicitement en paramètre au compilateur.

En fait l'argument de la directive #include peut aussi être une macro, dont la valeur est un argument valide (soit une chaîne de caractères, soit un nom entre balises < et >) :

#define FICHIER_A_INCLURE <stdio.h>
#include FICHIER_A_INCLURE

Exemple

  • fichier.h
void affiche( void );
  • fichier.c
#include "fichier.h"

int main( void )
{
        affiche();
        return 0;
}

C'est comme si fichier.c avait été écrit :

void affiche( void );

int main( void )
{
        affiche();
        return 0;
}

Protection contre les inclusions multiples

À noter un problème relativement récurrent avec les fichiers en-têtes : il s'agit des inclusions multiples. À mesure qu'une application grandit, il arrive fréquemment qu'un fichier soit inclus plusieurs fois à la suite d'une directive #include. Quand bien même les déclarations sont identiques, définir deux types avec le même nom n'est pas permis en C. On peut néanmoins s'en sortir avec cette technique issue de la nuit des temps :

#ifndef H_MON_FICHIER
#define H_MON_FICHIER

/* Mettre toutes les déclarations ici */

#endif

C'est un problème tellement classique, que le premier réflexe lors de l'écriture d'un tel fichier est de rajouter ces directives. H_MON_FICHIER est bien-sûr à adapter à chaque fichier, l'essentiel est que le nom soit unique dans toute l'application. Habituellement on utilise le nom du fichier, mis en majuscule, avec les caractères non-alphabétiques remplacés par des soulignés.

Avertissement et message d'erreur personnalisés

Il peut être parfois utile d'avertir l'utilisateur que certaines combinaisons d'options sont dangereuses ou carrément invalides. Le préprocesseur dispose d'une directive pour effectuer cela:

#error "message d'erreur"

Cette directive affichera en fait le message tel quel, qui peut évidemment contenir n'importe quoi, y compris des caractères réservés, sans qu'on ait besoin de le mettre entre guillemets ("). Après émission du message, la compilation s'arrêtera.

À noter que certains compilateurs comme GCC proposent aussi la directive:

#warning "message d'avertissement"

qui permet d'afficher un message sans arrêter la compilation.


Bibliothèque standard

La bibliothèque standard du langage C peut paraître relativement pauvre par rapport à d'autres langages tout en un plus récents comme Python, Ruby, Perl, C# ou Java. Conçue avant tout avec un souci de portabilité, et en ayant en tête les contraintes matérielles limitées de certaines plate-formes auxquelles le C est aussi destiné, vous obtenez avec cela le plus petit dénominateur commun qui puisse être porté sur le plus grand nombre de plateformes. Les contraintes qu'ont subies l'ANSI, et le WG14 après lui, portant principalement sur les contraintes de portabilité sur certaines architectures parfois « exotiques », et la très forte contrainte de ne pas « casser » du code existant reposant sur un comportement déjà établi, voire normalisé, font que certains points connus pour être complexes, voire peu souhaitables, sont restés dans le langage et la bibliothèque standard.

Concevoir une application avec les seules fonctions présentes dans cette bibliothèque nécessite une très grande rigueur. Le WG14 ne s'étant pas fixé pour but d'étendre la bibliothèque standard de manière importante, il est plus que conseillé de se tourner vers des bibliothèques de plus haut niveau, afin d'éviter de réinventer inutilement la roue. Il en existe heureusement beaucoup, mais décrire ne serait-ce que ce qui existe est hors de la portée de cet ouvrage.

La bibliothèque standard permet toutefois de faire des traitements usuels avec peu d'efforts, pour peu qu'on ait conscience des dangers et des pièges qui sont parfois tendus. Les sections qui suivent permettront de voir un peu plus clair dans les méandres parfois très sombres où s'aventure le C.

Voir aussi

Cours sur Wikiversity sur l'utilisation des fonctions standards du langage C.


Chaînes de caractères

Le langage C offre quelques facilités d'écritures pour simuler les chaînes de caractères à l'aide de tableaux. En plus de cela, certaines fonctions de la bibliothèque standard (et les autres) permettent de faciliter leur gestion. À la différence d'un tableau, les chaînes de caractères respectent une convention : se terminer par le caractère nul, noté '\0' (antislash-zero). Ainsi, pour construire une chaîne de caractères « à la main », il ne faut pas oublier ce caractère final.

On peut noter que, par rapport à ce qui est disponible dans d'autres langages, les fonctions de la bibliothèque standard sont peu pratiques à utiliser. On peut avancer sans trop de risque qu'une des plus grandes lacunes du C provient de sa gestion précaire et limitée des chaînes de caractères, pourtant massivement employées dans d'innombrables logiciels. De plus, une mauvaise utilisation de ces fonctions peut facilement conduire à es bugs. Pour faire court, on ne saurait trop conseiller de soit reprogrammer les fonctions ci-dessous, d'utiliser une bibliothèque externe ou de faire preuve de paranoïa avant leur utilisation.

On notera en particulier que la bibliothèque du langage C, assez ancienne n'apporte pas de fonction particulière pour traiter les spécificités de nouveaux standards tels qu'Unicode. Elle est toutefois assez générique pour effectuer certains opérations basiques. Pour une utilisation plus avancée, un bibliothèque Unicode peut avoir un intérêt.

Les fonctions permettant de manipuler les chaînes de caractères se trouvent dans l'entête <string.h>', ainsi pour les utiliser il faut ajouter la commande préprocesseur :

#include <string.h>

Comparaison de chaînes

int strcmp(const char * chaine1, const char * chaine2);
int strncmp(const char * chaine1, const char * chaine2, size_t longueur);

Compare les chaînes chaine1 et chaine2 et renvoie un entier :

  • négatif, si chaine1 est inférieure à chaine2 (avant dans l'ordre alphabétique) ;
  • nul, si chaine1 est égale à chaine2 (.i.e. chaine1 et chaine2 sont identiques) ;
  • positif, si chaine1 est supérieur à chaine2 (après dans l'ordre alphabétique) .

première remarque : lorsque les deux chaînes sont égales, strcmp renvoie 0, qui a la valeur de vérité faux. Pour tester l'égalité entre deux chaînes, il faut donc écrire soit if (strcmp(chaine1, chaine2) == 0) ..., soit if (!strcmp(chaine1, chaine2)) ... mais surtout pas if (strcmp(chaine1, chaine2)) ... qui teste si deux chaînes sont différentes !

seconde remarque : l'opérateur ==, dans le cas de pointeurs, teste si les adresses sont égales. Noter chaine1 == chaine2, si chaine1 et chaine2 sont des char * revient à tester si les deux chaînes pointent sur la même zone mémoire et non pas à tester l'égalité de leur contenu.

troisième remarque : la comparaison effectuée est une comparaison binaire. Deux chaînes canoniquement équivalentes au sens Unicode sont donc ici considérées différentes.

quatrième remarque : la comparaison n'est pas basée sur un alphabet mais sur un codage de caractère. Ceci conduit à un tri particulier des caractères accentuées, majuscules et minuscules,qui n'est pas nécessairement celui souhaité.

À noter l'existence de deux fonctions de comparaisons de chaîne, insensibles à la casse des caractères et s'adaptant à la localisation en cours, fonctionnant sur le même principe que strcmp() et strncmp(), mais dont l'origine provient des systèmes BSD :

int strcasecmp(const char * chaine1, const char * chaine2);
int strncasecmp(const char * chaine1, const char * chaine2, size_t longueur);

Ces fonctions se basent sur les locales [1].

Longueur d'une chaîne

size_t strlen(const char * chaine);

Renvoie la longueur de la chaine en octets, sans compter le marqueur de fin de chaîne '\0'.

exemple : strlen("coincoin") renvoie 8.

Nota: la longueur mesurée en caractères peut être différente de la longueur mesurée en octets suivant le système de codage de caractère utilisé. [2].

Concaténation et copie de chaînes

char * strcpy (char *destination, const char *source);
char * strncpy (char *destination, const char *source, size_t n);
char * strcat (char *destination, const char *source);
char * strncat (char *destination, const char *source, size_t n);

strcpy copie le contenu de source à l'adresse mémoire pointé par destination, incluant le caractère nul final. strcat concatène la chaîne source à la fin de la chaîne destination, en y rajoutant aussi le caractère nul final. Toutes ces fonctions renvoient le pointeur sur la chaîne destination.

Pour strncpy, on notera que la copie est limité à un nombre d'octets:

  • si la chaîne source a moins de n octets/caractères non nuls, strncpy complètera la chaîne destination avec des caractères nuls;
  • si la chaîne source a n octets/caractères non nuls ou plus, la fonction n'insèrera pas de caractère nul à la fin de la chaine destination (i.e. destination ne sera pas une chaîne de caractères valide).

Il faut être très prudent lors des copies ou des concaténations de chaînes, car les problèmes pouvant survenir peuvent être très pénibles à diagnostiquer. L'erreur « classique » est de faire une copie dans une zone mémoire non réservée ou trop petite, comme dans l'exemple suivant, a priori anodin:

Avertissement Ce code contient une erreur volontaire !
char *copie_chaine(const char *source)
{
    char *chaine = malloc(strlen(source));

    if (chaine != NULL)
    {
        strcpy(chaine, source);
    }
    return chaine;
}

Ce code a priori correct provoque pourtant l'écriture d'un caractère dans une zone non allouée. Les conséquences de ce genre d'action sont totalement imprévisibles, pouvant au mieux passer inaperçue, ou au pire écraser des structures de données critiques dans la gestion des blocs mémoires et engendrer des accès mémoire illégaux lors des prochaines tentatives d'allocation ou de libération de bloc, i.e. le cauchemar de tout programmeur. Dans cet exemple il aurait bien évidemment fallu écrire :

       char * chaine = malloc( strlen(source) + 1 );

Une autre erreur, beaucoup plus fréquente hélas, est de copier une chaîne dans un tableau de caractères local, sans se soucier de savoir si ce tableau est capable d'accueillir la nouvelle chaîne. Les conséquences peuvent ici être beaucoup plus graves qu'un simple accès illégal à une zone mémoire globale.

Un écrasement mémoire (buffer overflow) est considéré comme un défaut de sécurité relativement grave, puisque, sur un grand nombre d'architectures, un « attaquant » bien renseigné sur la structure du programme peut effectivement lui faire exécuter du code arbitraire. Ce problème vient de la manière dont les variables locales aux fonctions et certaines données internes sont stockées en mémoire. Comme le C n'interdit pas d'accéder à un tableau en dehors de ses limites, on pourrait donc, suivant la qualité de l'implémentation, accéder aux valeurs stockées au-delà des déclarations de variables locales. En fait, sur un grand nombre d'architectures, les variables locales sont placées dans un espace mémoire appelé pile, avec d'autres informations internes au système, comme l'adresse de retour de la fonction, c'est-à-dire l'adresse de la prochaine instruction à exécuter après la fin de la fonction. En s'y prenant bien, on peut donc écraser cette valeur pour la remplacer par l'adresse d'un autre bout de code, qui donnerait l'ordre d'effacer le disque dur, par exemple ! Si en plus le programme possède des privilèges, les résultats peuvent être assez catastrophiques. Ainsi décrit, le problème semble complexe : il faudrait que l'attaquant puisse insérer dans une zone mémoire de l'application un bout de code qu'il a lui-même écrit, et qu'il arrive à écrire l'adresse de ce bout de code là où le système s'attend à retrouver l'adresse de retour de la fonction. Cependant, ce genre d'attaque est aujourd'hui très courant, et les applications présentant ce genre d'erreur deviennent très rapidement la cible d'attaques.

Voici un exemple très classique, où ce genre d' exploit peut arriver :

Avertissement Ce code contient une erreur volontaire !
int traite_chaine(const char *ma_chaine)
{
    char tmp[512];

    strcpy(tmp, ma_chaine);

    /* ... */
}

Ce code, hélas plus fréquent qu'on ne le pense, est à bannir. Parmi différentes méthodes, on peut éviter ce problème en ne copiant qu'un certain nombre des premiers caractères de la chaîne, avec la fonction strncpy :

char tmp[512];
strncpy(tmp, ma_chaine, sizeof(tmp));
tmp[sizeof(tmp) - 1] = '\0';

On notera l'ajout explicite du caractère nul, si ma_chaine est plus grande que la zone mémoire tmp. La fonction strncpy ne rajoutant hélas pas, dans ce cas, de caractère nul. C'est un problème tellement classique que toute application C reprogramme en général la fonction strncpy pour prendre en compte ce cas de figure (voir la fonction POSIX strdup, ou strlcopy utilisée par le système d'exploitation OpenBSD, par exemple)

La limitation d'une chaîne à un nombre d'octets pose par contre des problèmes dans le cas des caractères multi-octets qui risquent alors e ne pas être entiers[2].

Le langage n'offrant que très peu d'aide, la gestion correcte des chaînes de caractères est un problème à ne pas sous-estimer en C.

Recherche dans une chaîne

char * strchr(const char * chaine, int caractère);
char * strrchr(const char * chaine, int caractère);

Recherche le caractère dans la chaine et renvoie la position de la première occurrence dans le cas de strchr et la position de la dernière occurrence dans le cas de strrchr.

char * strstr(const char * meule_de_foin, const char * aiguille);

Recherche l'aiguille dans la meule de foin et renvoie la position de la première occurrence.

Nota: Dans la cas de caractères multi-octets, cette fonction ne cherche qu'un morceau de caractère [2].

Traitement des blocs mémoire

La bibliothèque <string.h> contient encore quelques fonctions pour la manipulation de zone brute de mémoire. Ces fonctions, préfixées par mem, sont les équivalents des fonctions str* pour des zones de mémoire qui ne sont pas des chaînes de caractères. Cela peut être des tableaux non terminés par la valeur 0, comme des tableaux pouvant contenir la valeur 0 avant la fin, par exemple. Elles permettent aussi de traiter des structures (par exemple pour copier les données d'une structure dans une autre), ou des zones de mémoires allouées dynamiquement (par exemple pour initialiser une zone mémoire allouée par malloc).

Comme, au contraire des fonctions str*, elles n'ont pas de délimiteur de fin de tableau, il faut leur donner en paramètre la taille de ces tableaux (cette taille étant en bytes au sens strict, bien que souvent en octets).

void memcpy( void * destination, const void * source, size_t longueur );

Copie 'longueur' octet(s) de la zone mémoire 'source' dans 'destination'. Vous devez bien sûr vous assurer que la chaine destination ait suffisament de place, ce qui est en général plus simple dans la mesure où l'on connait la longueur.
Attention au récouvrement des zones : si destination + longueur < source alors source >= destination.

void memmove( void * destination, const void * source, size_t longueur )

Identique à la fonction memcpy(), mais permet de s'affranchir totalement de la limitation de recouvrement.

void memset( void * memoire, int caractere, size_t longueur );

Initialise les 'longueur' premiers octets du bloc 'memoire', par la valeur convertie en type char de 'caractere'. Cette fonction est souvent employée pour mettre à zéro tous les champs d'une structure ou d'un tableau :

struct MonType_t mem;

memset( &mem, 0, sizeof mem );

int memcmp( const void * mem1, const void * mem2, size_t longueur );

Compare les 'longueur' premiers octets des blocs 'mem1' et 'mem2'. Renvoie les codes suivants :

  • < 0 : mem1 < mem2
  • = 0 : mem1 == mem2
  • > 0 : mem1 > mem2
void * memchr( const void * memoire, int caractere, size_t longueur );

Recherche dans les 'longueur' premiers octets du bloc 'memoire', la valeur convertie en type char de 'caractere'. Renvoie un pointeur sur l'emplacement où le caractère a été trouvé, ou NULL si rien n'a été trouvé.

void * memrchr( const void * memoire, int caractere, size_t longueur );

Pareil que memchr(), mais commence par la fin du bloc 'memoire'.


Erreurs

Le langage C fournit un en-tête spécialisé pour la gestion des erreurs : <errno.h>. Cet en-tête déclare notamment une variable globale errno, et un certain nombre de codes d'erreur, qui permettent aux fonctions de la bibliothèque standard de reporter précisément la cause d'une erreur.

Utilisation

Pour inclure l'en-tête de son fichier source, il faut ajouter la ligne :

#include <errno.h>

On peut alors utiliser la variable errno, de type int, pour traiter les erreurs[3].

Lorsqu'on veut utiliser errno pour déterminer la cause d'un échec, il faut d'abord s'assurer que la fonction a bel et bien échoué. Le C laissant une certaine liberté dans la manière de signaler un échec, il n'y a pratiquement aucun mécanisme universel qui permette de détecter une telle situation, chaque fonction étant presque un cas particulier. Cependant, une pratique relativement répandue est de retourner un code spécifique, en général en dehors de l'intervalle de ce qu'on attend. Par exemple, lorsque la fonction est censée allouer un objet et retourne un pointeur, une erreur est souvent signalée en retournant un pointeur nul, et la variable errno décrit plus en détail la nature de l'erreur.

Il est nécessaire de placer errno à 0 avant d'utiliser la fonction qui peut échouer, car les fonctions de la bibliothèque standard ne sont pas obligées de la mettre à zéro en cas de succès. Si on ne la réinitialisait pas « manuellement », on pourrait « voir » le résultat d'une erreur causée par un appel de fonction antérieur. La procédure à suivre est donc la suivante :

  • Mettre errno à 0 ;
  • Appeler la fonction (de la bibliothèque standard) que l'on souhaite ;
  • Vérifier si elle a échoué ;
    • Si c'est le cas : la valeur de errno est disponible pour traiter l'erreur ;
    • Sinon : procéder à la suite du traitement.

Affichage d'un message d'erreur

Une fois qu'on est sûr que la variable errno contient une valeur pertinente, il est indispensable de déterminer le traitement à effectuer, et le cas échéant présenter un message tout aussi significatif à l'utilisateur. Rien n'est plus frustrant pour un utilisateur qu'un message aussi abscons que : "fopen("/tmp/tmpab560f9d4", "w") : erreur 28", alors qu'un message écrit sous la forme "impossible de créer le fichier /tmp/tmpab560f9d4: plus de place disponible sur le périphérique" sera plus facilement compris par l'utilisateur.

Énumérer toutes les valeurs possibles d'errno pour leur associer un message peut être relativement pénible, surtout si on doit le faire dans chaque application. Heureusement qu'une fonction toute prête existe :

#include <string.h>
char * strerror(int code);

Cette fonction permet de connaître la signification textuelle d'une valeur de errno. À noter que le code de retour est une chaine statique, dont il est sage de présupposer la durée de vie la plus courte possible. N'essayez pas non plus de modifier le contenu de la chaîne, affichez-la directement ou copiez-la dans une zone temporaire.

Un autre intérêt de passer par cette fonction est que les messages retournés sont adaptés à la localisation du système. Ce qui est loin d'être négligeable, car cela retire au programmeur le souci de traduire les messages d'erreurs dus à la bibliothèque standard dans toutes les langues possibles (chaque implémentation pouvant avoir ses propres codes d'erreur spécifiques, la maintenance d'une telle traduction serait un cauchemar).

Une autre fonction, perror, permet d'envoyer le message correspondant à errno sur le flux des erreurs stderr.

#include <errno.h>
void perror(const char *chaine);

Le paramètre chaine, s'il est différent de NULL, est une chaine de caractère passée par le programmeur et affichée avant le message d'erreur.

Exemple

Pour illustrer cela, voici un exemple, qui tente de convertir une chaine de caractères en un nombre. Le nombre donné, 264, peut ne pas être dans le domaine de valeur du type unsigned long (qui peut se limiter à 232 - 1), suivant l'architecture. Dans ce cas, on utilise errno pour afficher un message d'erreur approprié à l'utilisateur.

#include <stdio.h>  /* puts(), printf(), NULL */
#include <stdlib.h> /* strtoul() */
#include <limits.h> /* ULONG_MAX */
#include <string.h> /* strerror() */
#include <errno.h>  /* errno */

static void teste(const char *nombre)
{
    unsigned long res;

    /* On réinitialise errno */
    errno = 0;

    /* Appel de strtoul : conversion d'une chaine en un entier unsigned long*/
    res = strtoul(nombre, NULL, 10);

    /* On détecte une erreur de strtoul quand elle renvoie ULONG_MAX _et_
     * que errno est non nul. En effet, si on lui passe en entrée la
     * représentation de ULONG_MAX, la conversion se fait sans erreur
     * et errno reste à 0.
     */
    if (res == ULONG_MAX && errno != 0)
    {
        /* Il y a eu une erreur ! */
        (void)fprintf(stderr, "Impossible de convertir le nombre '%s': %s.\n",
            nombre, strerror(errno));
    }
    else
    {
       (void)printf("La conversion a été effectuée, et la valeur est: %lu\n", res);
    }
}

int main(void)
{
    /* 2 puissance 8 : sera toujours accepte */
    teste("256");

    /* 2^64 - 1 : peut echouer suivant la machine */
    teste("18446744073709551615");
    return EXIT_SUCCESS;
}

Si le type unsigned long est codé sur 32 bits, on peut obtenir:

La conversion a été effectuée, et la valeur est: 256
Impossible de convertir le nombre '18446744073709551615': la valeur est en dehors de l'intervalle du type spécifié.

Notons bien qu'il est nécessaire de placer errno à 0 avant d'utiliser strtoul, car elle pourrait contenir une valeur non nulle, même au lancement du programme.


Mathématiques

Pour pouvoir utiliser les fonctions mathématiques, il faut utiliser l'en-tête <math.h>, ainsi que <errno.h> pour gérer les erreurs :

#include <math.h>
#include <errno.h>

Comme pour un certain nombre de fonctions de la bibliothèques standard, il est en effet nécessaire d'utiliser <errno.h> pour détecter l'erreur d'une fonction mathématique (voir le chapitre sur la gestion d'erreurs pour voir comment utiliser <errno.h>).

Note : Sur certains compilateurs comme GCC, il est nécessaire d'ajouter durant l'édition des liens une option pour que la « librairie mathématique » soit liée au programme. Pour GCC, cette option est -lm. Sans cette option, le programme pourra compiler, mais le résultat à l'exécution sera surprenant...

Exponentiations

double exp ( double x );
double pow ( double x, double y );

exp calcule e élevé à la puissance de x ( [image]) où e est la base des logarithmes naturels (ln(e) = 1). pow calcule la valeur de x élévé à la puissance y ( [image]).

Erreurs

La fonction pow peut déclencher l'erreur suivante :

  • EDOM : x est négatif, et y n’est pas un entier.

Logarithmes

double log ( double x );
double log10 ( double x );

log calcule le logarithme néperien de x (noté généralement ln(x) en mathématiques). log10 calcule le logarithme à base 10 de x.

Erreurs

  • EDOM : x est négatif ;
  • ERANGE : x est nul.

Racine carrée

double sqrt ( double x );

Renvoie la racine carrée de x.

Erreurs

  • EDOM : x est négatif.

Sinus, cosinus, tangente

double sin ( double x );
double cos ( double x );
double tan ( double x );

Note : les angles retournés sont en radians (intervalle -pi/2 à pi/2).

Arc sinus, arc cosinus

double asin ( double x );
double acos ( double x );

Erreurs

  • EDOM : x est inférieur à -1 ou supérieur à 1.

Arc tangente

double atan ( double x );
double atan2 ( double y, double x );

Gestion de la mémoire

La gestion dynamique de la mémoire en C se fait à l'aide de principalement deux fonctions de la bibliothèque standard :

  • malloc, pour l'allocation dynamique de mémoire ;
  • free, pour la libération de mémoire préalablement allouée avec malloc.

Deux autres fonctions permettent de gérer plus finement la mémoire :

  • calloc, pour allouer dynamiquement de la mémoire, comme malloc, qui a préalablement été initialisée à 0 ;
  • realloc, pour modifier la taille d'une zone mémoire déjà allouée.

Ces fonctions sont déclarées dans l'en-tête <stdlib.h>.

Gestion dynamique de la mémoire

Les déclarations de variables en C et dans beaucoup d'autres langages ont une limitation très importante : la taille des variables doit être connue à la compilation. Cela pose problème quand on ne sait qu'à l'exécution le nombre de données qu'on doit traiter. Pour résoudre ce problème, et pouvoir décider durant l'exécution d'un programme du nombre de variables à créer, il faudra nécessairement passer par de l'allocation dynamique de mémoire. Avec ce système, le programmeur dispose de fonctions qui permettent de demander au système une zone mémoire d'une certaine taille, qu'il pourra utiliser comme il le souhaite. En C, ces fonctions sont disponibles dans l'en-tête <stdlib.h>.

L'un des principaux avantages qu'offre le langage C est sa capacité à fournir au programmeur un contrôle poussé sur la gestion de la mémoire. Cette liberté nécessite néanmoins une grande rigueur, tant les problèmes pouvant survenir sont nombreux et souvent difficiles à diagnostiquer. On peut dire sans prendre beaucoup de risque que la plupart des erreurs de programmation en C, ont pour origine une mauvaise utilisation des fonctions de gestion de la mémoire. Il ne faut pas sous-estimer la difficulté de cette tâche. Autant cela est trivial pour un programme de quelques centaines de lignes, autant cela peut devenir un casse-tête quand ledit programme a subi des changements conséquents, pas toujours faits dans les règles de l'art.

La manière dont la mémoire physique d'un ordinateur est conçue, ainsi que la façon dont le système d'exploitation la manipule, sont très variables. Cependant, un modèle assez classique consiste à découper la mémoire en segments, segments dont on garde les références dans des tables de pages : c'est le modèle de segmentation / pagination. Ce modèle offre beaucoup d'avantages par rapport à un accès purement linéaire. Décrire son fonctionnement en détail est hors de la portée de cet ouvrage, mais on pourra noter tout de même :

  • Indépendance totale de l'espace d'adressage entre les processus : un processus ne peut pas accéder à la mémoire d'un autre processus. C'est pourquoi transmettre la valeur d'un pointeur à un autre processus ne servira en général à rien, car le second processus ne pourra jamais accéder à l'emplacement pointé.
  • Gestion fine de la mémoire : les segments sont accédés via plusieurs niveaux d'indirection dans les tables de pages. Cela permet de mettre virtuellement les segments n'importe où dans la mémoire ou même sur un système de fichier. Dans la pratique, éparpiller trop les segments (fragmenter) réduit significativement les performances.

Au niveau des inconvénients, on citera essentiellement un problème de performances. Plusieurs niveaux d'indirection impliquent de multiple lectures en mémoire, extrêmement pénalisant en terme de temps d'exécution, au point où des caches sont nécessaires pour garantir des vitesses acceptables. Même si RAM veut dire mémoire à accès aléatoire, il faut bien garder à l'esprit qu'un accès purement séquentiel (adresse mémoire croissante) peut être jusqu'à cent fois plus rapide qu'une méthode d'accès qui met sans cesse à défaut ce système de cache.

Un processus peut donc demander au système de réserver pour son usage exclusif un secteur mémoire de taille déterminée. Il peut également demander au système de modifier la taille d'une zone précédemment réservée ou de la libérer s'il n'en a plus l'utilité.


Fonctions fournies par

Fonctions fournies par <stdlib.h>

malloc: allocation de mémoire

La fonction la plus simple d'allocation mémoire est malloc :

#include <stdlib.h>

void * malloc(size_t taille);

Elle prend en argument la taille que l'on veut allouer et renvoie un pointeur vers une zone mémoire allouée ou un pointeur nul si la demande échoue (la cause d'erreur la plus courante étant qu'il n'y a plus assez d'espace mémoire disponible). Trois remarques peuvent être faites :

  1. malloc renvoie une valeur de type void *, il n'est pas nécessaire de faire une conversion explicite (cela est nécessaire en C++) ;
  2. l'argument taille est de type size_t, l'appel à la fonction malloc devrait toujours se faire conjointement à un appel à l'opérateur sizeof.
  3. La zone mémoire allouée, si l'appel réussit, n'est pas initialisée. C'est une erreur de conception grave que d'accéder au bloc mémoire en s'attendant à trouver une certaine valeur (0 par exemple).

Par exemple, si on souhaite réserver une zone mémoire pour y allouer un entier :

/* Déclaration et initialisation */
int *ptr = NULL;

/* Allocation */
ptr = malloc(sizeof(int));

/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
{
    /* Stockage de la valeur "10" dans la zone mémoire pointée par ptr */
    *ptr = 10;
}
else
{
    /* décider du traitement en cas d'erreur */
}

Si on souhaite allouer de l'espace pour une structure de données plus complexe :

typedef struct{
    double b;
    int a;
    MaStructure *suivant;
} MaStructure;

/* Déclaration et initialisation */
MaStructure *ptr = NULL;

/* Allocation */
ptr = (MaStructure *)malloc(sizeof(MaStructure));

if (ptr != NULL)
{
    /* Initialisation de la structure nouvellement créée */
    ptr->a = 10;
    ptr->b = 3.1415;
    ptr->suivant = NULL;
}

free: libération de mémoire

Le C ne possède pas de mécanisme de ramasse-miettes, la mémoire allouée dynamiquement par un programme doit donc être explicitement libérée. La fonction free permet de faire cette libération.

void free( void * pointeur );

La fonction prend en argument un pointeur vers une zone mémoire précédemment allouée par un appel à malloc, calloc ou realloc et libère la zone mémoire pointée.

Exemple :

/* Déclaration et initialisation */
int *ptr = NULL;

/* Allocation */
ptr = malloc(sizeof(int));

/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
{
    /* ... utilisation de la zone allouée ... */

    /* Libérer la mémoire utilisée */
    free(ptr);
    ptr = NULL; /* Pour éviter les erreurs */
}
else
{
    /* décider du traitement en cas d'erreur */
}

L'utilisation du pointeur après libération de la zone allouée (ou la double libération d'une même zone mémoire) est une erreur courante qui provoque des résultats imprévisibles. Il est donc conseillé :

  • d'attribuer la valeur nulle (NULL) au pointeur juste après la libération de la zone pointée, et à toute autre pointeur faisant référence à la même adresse,
  • de tester la valeur nulle avant toute utilisation d'un pointeur.

De plus, donner à free l'adresse d'un objet qui n'a pas été alloué par une des fonctions d'allocation cause un comportement indéfini.

calloc : allocation avec initialisation à 0

La fonction calloc permet d'allouer une zone mémoire dont tous les bits seront initialisés à 0. Son prototype est légèrement différent de celui de malloc, et est pratique pour l'allocation dynamique de tableaux.

Syntaxe :

void * calloc(size_t nb_element, size_t taille);

De manière similaire à malloc, calloc retourne un pointeur de type void* pointant une zone de nb_element*taille octets allouée en mémoire, dont tous les bits seront initialisés à 0, ou retourne un pointeur nul en cas d'échec.

Exemple :

/* allouer un tableau de 5 entiers */
int* ptr = calloc ( 5, sizeof(int) );

Le pointeur contient l'adresse du premier élément du tableau :

*ptr = 3;   /* premier entier : 3  */

Le pointeur peut être utilisé comme un tableau classique pour accéder aux éléments qu'il contient :

ptr[0] = 3; /* équivaut à  *ptr = 3;      */
ptr[1] = 1; /* équivaut à  *(ptr+1) = 1;  */
ptr[2] = 4; /* équivaut à  *(ptr+2) = 4;  */

Notez que calloc place tous les bits à zéro, mais que ce n'est pas nécessairement une représentation valide pour un pointeur nul ni pour le nombre zéro en représentation flottante. Ainsi, pour initialiser à zéro un tableau de double de manière portable, par exemple, il est nécessaire d'assigner la valeur 0.0 à chaque élément du tableau. Étant donné qu'on initialise chaque élément « manuellement », on peut dans ce cas utiliser malloc plutôt que calloc (la première étant normalement beaucoup plus rapide que la seconde).

realloc

Il arrive fréquemment qu'un bloc alloué n'ait pas la taille suffisante pour accueillir de nouvelles données. La fonction realloc est utilisée pour changer (agrandir ou réduire) la taille d'une zone allouée par malloc, calloc, ou realloc.

Syntaxe :

void * realloc(void * ancien_bloc, size_t nouvelle_taille);

realloc tentera de réajuster la taille du bloc pointé par ancien_bloc à la nouvelle taille spécifiée. À noter :

  • si nouvelle_taille vaut zéro, l'appel est équivalent à free(ancien_bloc).
  • si ancien_bloc est nul, l'appel est équivalent à malloc(nouvelle_taille).
  • En cas de succès, realloc alloue un espace mémoire de taille nouvelle_taille, copie le contenu pointé par le paramètre pointeur dans ce nouvel espace (en tronquant éventuellement si la nouvelle taille est inférieure à la précédente), puis libère l'espace pointé et retourne un pointeur vers la nouvelle zone mémoire.
  • En cas d'échec, cette fonction ne libère pas l'espace mémoire actuel, et retourne une adresse nulle.

Notez bien que realloc ne peut que modifier des espaces mémoires qui ont été alloués par malloc, calloc, ou realloc. En effet, autoriser realloc à manipuler des espaces mémoires qui ne sont pas issus des fonctions de la bibliothèque standard pourrait causer des erreurs, ou des incohérences graves de l'état du processus. En particulier, les tableaux, automatiques comme statiques, ne peuvent être passés à realloc, comme illustré par le code suivant :

Avertissement Ce code contient une erreur volontaire !
void f(void)
{
    int tab[10];
    /* ... */
    int *ptr = realloc(tab, 20 * sizeof(int));
    /* ... */
}

Lorsque realloc reçoit la valeur de tab, qui est un pointeur sur le premier élément (i.e. &tab[0]), il ne peut la traiter, et le comportement est indéfini. Sur cet exemple, il est facile de voir l'erreur, mais dans l'exemple suivant, la situation est plus délicate :

#include <stdint.h> /* pour SIZE_MAX */
#include <stdlib.h>

/* 'double' essaye de doubler l'espace mémoire pointé par ptr.
 *
 * En cas de succès, la valeur renvoyée est un pointeur vers le nouvel espace mémoire, et l'ancienne
 * valeur de ptr est invalide.
 * En cas d'échec, l'espace pointé par ptr n'est pas modifié, et la valeur NULL est renvoyée.
 */
void *double(void *ptr, size_t n)
{
    void *tmp = NULL;
    if ((ptr != NULL) && (n != 0) && (n <= SIZE_MAX / 2))
    {
        tmp = realloc(ptr, 2 * n);
    }
    return tmp;
}

La fonction double en elle-même ne comporte pas d'erreur, mais elle peut causer des plantages suivant la valeur de ptr qui lui est passée. Pour éviter des erreurs, il faudrait que la documentation de la fonction précise les contraintes sur la valeur de ptr... et que les programmeurs qui l'utilisent y fassent attention.

On peut aussi noter que, quand realloc réussit, le pointeur renvoyé peut très bien être égal au pointeur initial, ou lui être différent. En particulier, il n'y a aucune garantie que, quand on diminue la taille de la zone mémoire, il le fasse « sur place ». C'est très probable, car c'est ce qui est le plus facile et rapide à faire du point de vue de l'implémentation, mais rien ne l'empêche par exemple de chercher un autre espace mémoire disponible qui aurait exactement la taille voulue, au lieu de garder la zone mémoire initiale.

On peut noter le test (n <= SIZE_MAX / 2). Il permet d'éviter un débordement entier : si n était supérieur à cette valeur, le produit n * 2 devrait avoir une valeur supérieure à SIZE_MAX, qui est la plus grande valeur représentable par le type size_t. Lorsque cette valeur est passée à realloc, la conversion en size_t, qui est un type entier non signé, se fera modulo SIZE_MAX + 1, et donc la fonction recevra une valeur différente de celle voulue. Si le test n'était pas fait, double pourrait ainsi retourner à l'appelant une zone mémoire de taille inférieure à celle demandée, ce qui causerait des bogues. Ce genre de bogue (non spécifique à realloc) est très difficile à détecter, car n'apparaît que lorsque l'on atteint des valeurs limites, ce qui est assez rare, et le problème peut n'être visible que bien longtemps après que l'erreur de calcul soit faite.

Gestion d'erreur

Pour gérer correctement le cas d'échec, on ne peut faire ainsi :

Avertissement Ce code contient une erreur volontaire !
int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
{
    /* ... */

    /* On se rend compte qu'on a besoin d'un peu plus de place. */
    ptr = realloc(ptr, 20 * sizeof(int));
    /* ... */
}

En effet, si realloc échoue, la valeur de ptr est alors nulle, et on aura perdu la référence vers l'espace de taille 10 * sizeof(int) qu'on a déjà alloué. Ce type d'erreur s'appelle une fuite mémoire. Il faut donc faire ainsi :

int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
{
    /* ... */

    /* On se rend compte qu'on a besoin d'un peu plus de place. */
    int *tmp = realloc(ptr, 20 * sizeof(int));
    if (tmp == NULL)
    {
        /* Exemple de traitement d'erreur minimaliste */
        free(ptr);
        return EXIT_FAILURE;
    }
    else
    {
        ptr = tmp;
        /* On continue */

    }
}

Exemple

realloc peut être utilisé quand on souhaite boucler sur une entrée dont la longueur peut être indéfinie, et qu'on veut gérer la mémoire assez finement. Dans l'exemple suivant, on suppose définie une fonction lire_entree qui :

  • lit sur une entrée quelconque (par exemple stdin) un entier, et renvoie 1 si la lecture s'est bien passée ;
  • renvoie 0 si aucune valeur n'est présente sur l'entrée, ou en cas d'erreur.

Cette fonction est utilisée pour construire un tableau d'entiers issus de cette entrée. Comme on ne sait à l'avance combien d'éléments on va recevoir, on augmente la taille d'un tableau au fur et à mesure avec realloc.

/* Fonction qui utilise 'lire_entree' pour lire un nombre indéterminé
 * d'entiers.
 * Renvoie l'adresse d'un tableau d'entiers (NULL si aucun entier n'est lu).
 * 'taille' est placé au nombre d'éléments lus (éventuellement 0).
 * 'erreur' est placée à une valeur non nulle en cas d'erreur, à une valeur nulle sinon.
 */
int *traiter_entree(size_t *taille, int *erreur)
{
    size_t max = 0;   /* Nombre d'éléments utilisables */
    size_t i = 0;     /* Nombre d'éléments utilisés */
    int *ptr = NULL;  /* Pointeur vers le tableau dynamique */
    int valeur;       /* La valeur lue depuis l'entrée */
    *erreur = 0;

    while (lire_entree(&valeur) != 0)
    {
        if (i >= max)
        {
            /* Il n'y a plus de place pour stocker 'valeur' dans 'ptr[i]' */
            max = max + 10;
            int *tmp = realloc(ptr, max * sizeof(int));
            if (tmp == NULL)
            {
                /* realloc a échoué : on sort de la boucle */
                *erreur = 1;
                break;
            }
            else
            {
                ptr = tmp;
            }
        }
        ptr[i] = valeur;
        i++;
    }
    *taille = i;
    return ptr;
}

Ici, on utilise max pour se souvenir du nombre d'éléments que contient la zone de mémoire allouée, et i pour le nombre d'éléments effectivement utilisés. Quand on a pu lire un entier depuis l'entrée, et que i vaut max, on sait qu'il n'y a plus de place disponible et qu'il faut augmenter la taille de la zone de mémoire. Ici, on incrémente la taille max de 10 à chaque fois, mais il est aussi possible de la multiplier par 2, ou d'utiliser toute autre formule. On utilise par ailleurs le fait que, quand le pointeur envoyé à realloc est nul, la fonction se comporte comme malloc.

Le choix de la formule de calcul de max à utiliser chaque fois que le tableau est rempli résulte d'un compromis :

  • augmenter max peu à peu permet de ne pas gaspiller trop de mémoire, mais on appellera realloc très souvent.
  • augmenter très vite max génère relativement peu d'appels à realloc, mais une grande partie de la zone mémoire peut être perdue.

Une allocation mémoire est une opération qui peut être coûteuse en terme de temps, et un grand nombre d'allocations mémoire peut fractionner l'espace mémoire disponible, ce qui alourdit la tâche de l'allocateur de mémoire, et au final peut causer des pertes de performance de l'application. Aucune formule n'est universelle, chaque situation doit être étudiée en fonction de différents paramètres (système d'exploitation, capacité mémoire, vitesse du matériel, taille habituelle/maximale de l'entrée...). Toutefois, l'essentiel est bien souvent d'avoir un algorithme qui marche, l'optimisation étant une question secondaire. Dans une telle situation, utilisez d'abord une méthode simple (incrémentation ou multiplication par une constante), et n'en changez que si le comportement du programme devient gênant.


Problèmes et erreurs classiques

Défaut d'initialisation d'un pointeur

Pour éviter des erreurs, un pointeur devrait toujours être initialisé lors de sa déclaration ; soit à NULL, soit avec l'adresse d'un objet, soit avec la valeur de retour d'une fonction « sûre » comme malloc. Méditons sur l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
/* Déclaration sans initialisation */
int *ptr;
int var;

/* On stocke dans var la valeur de la zone mémoire pointée par ptr*/
var = *ptr;

Ce code va compiler! Si on est chanceux l'exécution de ce code provoquera une erreur à la troisième étape lorsqu'on déréférence ptr. Si on l'est moins, ce code s'exécute sans souci et stocke dans var une valeur aléatoire, ce qui pourrait provoquer une erreur lors d'une utilisation ultérieure de la variable var alors que l'erreur réelle se situe bien plus en amont dans le code! Une variable (automatique) de type pointeur est en effet comme toutes les autres variables : si elle n'est pas initialisée explicitement, son contenu est indéfini ; son déréférencement peut donc causer n'importe quel comportement.

Lorsqu'on déclare une variable ou qu'on alloue une zone mémoire, on ne dispose d'aucune garantie sur le contenu de cette zone ou de cette variable. Initialiser systématiquement les variables dès leur déclaration, en particulier lorsqu'il s'agit de pointeurs, fait partie des bons réflexes à prendre et pour la plupart des applications ne dégrade pas le temps d'exécution de manière significative. Ce type d'erreur peut être extrèmement difficile à localiser à l'intérieur d'un programme plus complexe.

Référence multiple à une même zone mémoire

La recopie d'un pointeur dans un autre n'alloue pas une nouvelle zone. La zone est alors référencée par deux pointeurs. Un problème peut survenir si on libère la zone allouée sans réinitialiser tous les pointeurs correspondants :

Avertissement Ce code contient une erreur volontaire !
int* ptr = malloc(sizeof(int)); // allouer une zone mémoire pour un entier
int* ptr2 = ptr; // ptr2 pointe la même zone mémoire

*ptr = 5;
printf("%d\n", *ptr2 ); // affiche 5

/* libération */
free( ptr );
ptr = NULL;

*ptr2 = 10; /* <- résultat imprévisible (plantage de l'application, ...) */

En règle générale, il faut éviter que plusieurs variables pointent la même zone mémoire allouée.

L'utilisation d'un outil de vérification statique permet de détecter ce genre d'erreur.

Fuite mémoire

La perte du pointeur associé à un secteur mémoire rend impossible la libération du secteur à l'aide de free. On qualifie cette situation de fuite mémoire, car des secteurs demeurent réservés sans avoir été désalloués. Cette situation perdure jusqu'à la fin du programme principal. Voyons l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
int i = 0;       // un compteur
int* ptr = NULL; // un pointeur
while (i < 1001) {
    ptr = malloc(sizeof(int)); // on écrase la valeur précédente de ptr par une nouvelle
    if (ptr != NULL)
    {
       /* traitement ....*/
    }
}

À la sortie de cette boucle on a alloué un millier d'entiers soit environ 2000 octets, que l'on ne peut pas libérer car le pointeur ptr est écrasé à chaque itération (sauf la dernière). La mémoire disponible est donc peu à peu grignotée jusqu'au dépassement de sa capacité. Ce genre d'erreurs est donc à proscrire pour des processus tournant en boucle pendant des heures, voire indéfiniment. Si son caractère cumulatif la rend difficile à détecter, il existe de nombreux utilitaires destinés à traquer la moindre fuite.


Gestion des signaux

Ébauche
Cette page est considérée comme une ébauche à compléter. Si vous possédez quelques connaissances sur le sujet, vous pouvez les partager en éditant dès à présent cette page (en cliquant sur le lien « modifier »).

Ressources suggérées : Aucune (vous pouvez indiquer les ressources que vous suggérez qui pourraient aider d'autres personnes à compléter cette page dans le paramètre « ressources » du modèle? engendrant ce cadre)

Les signaux permettent une communication, assez sommaire, entre le système et un processus, ou entre différents processus. Cette communication est sommaire, car un signal ne porte qu'une seule information: son numéro, de type int. Un processus peut aussi s'envoyer un signal à lui-même.

Ces signaux sont envoyés de manière asynchrone: lorsqu'un processus reçoit un signal, son exécution est interrompue, et une fonction spécifique, dite gestionnaire de signal, est appelée, avec en paramètre le numéro du signal reçu, pour traiter l'événement. Lorsque cette fonction se termine, le processus reprend là où il s'était arrêté.

C'est à chaque processus de déterminer ce qu'il fait quand il reçoit un signal de numéro donné, en définissant un gestionnaire de signal pour tous les signaux qu'il le souhaite. Vous l'aurez remarqué, dans tous les exemples de ce livre rencontrés jusqu'à présent, nous ne nous sommes pas occupés de savoir quels signaux nous pourrions recevoir, et comment il fallait les traiter. En l'absence de précisions dans nos programmes, l'implémentation fournira en effet un gestionnaire de signal par défaut, qui le plus souvent se contentera d'arrêter le programme.

Les fonctions suivantes, fournies par l'en-tête <signal.h>, sont utilisées dans la gestion des signaux :

  • signal() pour définir un gestionnaire de signal;
  • et raise() pour envoyer un signal au processus courant.

Le C définit ces deux fonctions, et pas plus, alors que les signaux peuvent faire beaucoup plus... En particulier, aucune fonction pour envoyer un signal à un autre processus n'est définie. Cela est dû au fait que, sur certains systèmes, la notion de processus n'existe pas, et une seule tâche s'exécute. Dans de telles situations, il serait aberrant de demander à un compilateur C pour un tel système de fournir des moyens de communications inter-processus ! Par contre, les communications inter-processus étant chose courante dans de nombreux autres systèmes, des extensions fournissent des moyens de le faire (voir par exemple la fonction kill() définie dans POSIX, cf. le chapitre Gestion des signaux du livre Programmation POSIX).

Définir un gestionnaire de signal

La fonction

void (*signal(int sig, void (*func)(int)))(int);

permet de définir un gestionnaire de signal. La signature de cette fonction étant un peu complexe, on peut la simplifier en utilisant un typedef:

typedef void (*t_handler)(int);

t_handler signal(int sig, t_handler func);

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

Ainsi, on voit mieux comment fonctionne cette fonction (notez que le type t_handler n'est pas défini par la norme, et n'est ici que pour clarifier la syntaxe). Elle prend deux paramètres:

  • le numéro du type de signal à traiter;
  • et un pointeur vers la fonction de gestion du signal.

Le premier paramètre est le numéro du signal. La norme définit un faible nombre de signaux, par des macros dans l'en-tête <signal.h>, par exemple SIGSEGV qui indique un accès illégal à la mémoire (SEGmentation Violation), et laisse à l'implémentation la liberté de définir d'autres types de signaux.

Pour le deuxième paramètre, on peut donner trois valeurs possibles:

  • SIG_IGN (macro définie dans <signal.h>): tout signal ayant le numéro sig sera ignoré (i.e. rien ne se passera, le programme continue de s'exécuter);
  • SIG_DFL (id.): le gestionnaire de signal par défaut sera mis en place pour ce type de signal.
  • un pointeur vers une fonction de type t_handler: cette fonction sera appelée lorsqu'un signal de type sig sera reçu par le processus.

Cette fonction renvoie la valeur du dernier gestionnaire de signal pour le numéro donné (qui peut être SIG_IGN ou SIG_DFL).

En cas d'erreur, la fonction renvoie SIG_ERR et place la variable errno à une valeur significative.

Les gestionnaires de signaux

Les gestionnaires de signaux sont des fonctions au prototype simple :

void fonction(int);

Elles prennent un argument de type int, qui est le numéro du signal, et ne renvoient rien. En effet, ces fonctions étant appelées de manière asynchrone, leur éventuelle valeur de retour ne serait récupérée, et encore utilisée, par personne...

Comme on utilise signal pour associer à chaque numéro de signal une fonction qui va gérer les signaux de ce numéro, on pourrait penser qu'il n'est pas nécessaire de donner ce numéro en paramètre à la fonction. Cependant cela s'avère pratique, car on peut ainsi définir une seule fonction qui peut gérer plusieurs types de signaux, et dont le comportement variera en fonction du signal à traiter.

Voici un exemple simple de définition et de mise en place d'un gestionnaire de signaux, pour le type de signal SIG_FPE, qui concerne des exceptions lors des calculs en virgule flottante (Floating Point Exception):

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

#include <signal.h>
#include <stdio.h>

void sig_fpe(int sig)
{
    /* ... */
}

int main(void)
{
    if (signal(SIG_FPE, sig_fpe) == SIG_ERR)
    {
        puts("Le gestionnaire de signal pour SIG_FPE n'a pu etre defini.");
    }
    else
    {
        puts("Le gestionnaire de signal pour SIG_FPE a pu etre defini.");
    }
    /* ... */
    return 0;
}


Conclusion

Le C est un langage plein de paradoxes. Pouvant aussi bien s'accommoder d'applications bas niveau que proposer des interfaces relativement proches des méthodes orientées objets, ce langage a de quoi séduire un large public. Pourtant, force est de constater qu'il a fait souffrir beaucoup de personnes et pas seulement les programmeurs. Issu de l'époque où la mémoire et les capacités de calcul étaient encore des denrées rares et chères, les pionniers de la programmation ont trop souvent privilégié le côté bas niveau de ce langage. Il en résulta des applications difficilement maintenables, abusant des aspects les plus complexes ou non-portables qu'offre le C pour économiser le moindre cycle processeur. C'est de cette époque que sa réputation « d'assembleur plus compliqué et plus lent que l'assembleur » s'est forgée, éclipsant ses aspects de plus haut niveau, qui pourtant méritent une meilleure estime.

Les fonctions de la bibliothèque standard et surtout le langage lui-même contiennent beaucoup de « pièges », notamment par les comportements indéfinis, ou dépendants de l'implémentation, qui rendent des bogues parfois très difficiles à découvrir. Le programmeur doit comprendre que le C lui fournit de grandes possibilités, mais qu'en retour il lui demande une rigueur d'autant plus grande. Apprivoiser ce langage peut nécessiter une longue phase d'apprentissage. Nous espérons que cet ouvrage vous aura permis d'appréhender le C avec plus de sérénité et de casser certains mythes qui ont décidément la vie bien dure.

Il existe beaucoup de bibliothèques qui proposent au programmeur C des outils pour étendre la bibliothèque standard, et permettent ainsi la gestion d'interfaces graphiques, la programmation réseau, le gestion de l'internationalisation, etc. Celles-ci sont très riches, et bien trop nombreuses pour être abordées dans ce livre. Wikilivres propose des livres étudiant certaines de ces bibliothèques. Pour les autres, Internet et ses moteurs de recherche restent de loin les meilleurs outils pour trouver ce dont vous aurez nécessairement besoin.

Le langage C a su s'adapter, d'Unix aux téléphones mobiles, de l'ISO-646 à Unicode. Le C est aujourd'hui un langage encore très utilisé. C'est LE langage de développement de la plupart des micro-contrôleurs actuels. Il a remplacé l'assembleur pour les petites applications. Pour les « grosses » applications tournant sur des OS (Windows, Linux, etc…), il est concurrencé par d'autres langages comme son dérivé le C++, un langage objet encore plus subtil mais permettant tout de même le développement d'applications complexes.

Voir aussi

  • Autotools en C

Notes

  1. ↑ www.freebsd.org/cgi/man.cgi?query=strcasecmp&sektion=3
  2. ↑ 2,0, 2,1 et 2,2 Par exemple, la caractère «é» peut être codé sur deux octets, en utf8, avec la séquence c3 a9 (pour en savoir plus, lire par exemple le wikilivre À la découverte d'Unicode)
  3. ↑ errno peut être définie par une macro ou un identificateur. N'essayez donc pas de récupérer son adresse.

Bibliographie

Vous trouverez ici des références d'ouvrages ou de sites internet ayant servi à l'élaboration de ce wikilivre, ou pouvant être d'intérêt pour une connaissance plus approfondie du C.

Site officiel du WG14 et références normatives

  • Le site officiel du WG14 anglais. Contient de nombreuses informations sur le C, dont les dernières propositions en discussion pour l'évolution de la norme. Attention toutefois, ces documents sont globalement très techniques. On peut noter la présence du Rationale pour la norme C99, qui lui est abordable.
    • Le TC3 n'est pas disponible sur le site du WS, mais sur celui de l'ISO.
  • Rationale de la norme ANSI X3.159-1989 anglais (absent du site du WG14)
  • La page du wiki du groupe de discussion comp.lang.c référençant les documents normatifs et les moyens d'acquérir la norme anglais.

Livres

  • Le « K&R »:
    • Brian W. Kernighan et Dennis M. Ritchie. Le Langage C: Norme ANSI, 2e éd., Dunod, (ISBN 2-100-05116-4) [présentation en ligne]
    • anglais Brian W. Kernighan et Dennis M. Ritchie. The C Programming Language, Second Edition. Prentice Hall, Inc., 1988. (ISBN 0-13-110362-8) [présentation en ligne]
  • Le site Developpez.com propose des critiques de livres sur le C ici (la page contient aussi des livres sur le C++).

Compilateurs C

  • Le wiki de comp.lang.c liste ici un certain nombre de compilateurs en précisant leur support des normes C90 et C99 anglais.

Autres ressources

Tutoriels

  • Les bases du langage C par William Waisse.

Histoire du C

  • The Development of the C Language par Dennis Ritchie anglais.
  • Historique de C par Marc Mongenet.

Licence

Licence de documentation libre GNU

GFDL GFDL Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture.
Récupérée de « http://fr.wikibooks.org/w/index.php?title=Programmation_C/Version_imprimable&oldid=445563 »
Catégories :
  • Versions imprimables
  • Programmation C (livre)
Catégories cachées :
  • À faire
  • Ébauches sans ressources suggérées
  • Wikilivres:ébauches


D:\_Essais II\le-langage-c.rtf

Le langage C

Vous souhaitez apprendre à programmer, mais vous ne savez pas comment vous y prendre ? Ou bien vous voulez réviser ? Alors, permettez-
nous de vous souhaiter la bienvenue dans ce cours de programmation en C pour débutants. La programmation est devenue aujourd’hui incontournable, si bien
qu’elle est utilisée partout. Tous les logiciels de votre ordinateur ont été programmés. Et ce cours va vous apprendre les bases de la programmation en C
pour vous permettre de créer des programmes à votre tour.
Pour pouvoir suivre ce tutoriel, vous n’avez aucun pré-requis ; tout sera détaillé de la manière la plus complète possible. Nous commencerons par une introduc-
tion à la programmation et au C, puis nous avancerons peu à peu dans l’univers de la programmation, tout cela grâce à des cours, des exemples, des exercices
d’applications et des travaux pratiques.

Introduction

Cette première partie sera l’occasion de se familiariser avec les concepts de base de la programmation et du C et de pouvoir commencer à pratiquer en écrivant
quelques programmes. Lisez-la bien attentivement, même si vos avez déjà une expérience en programmation, car il est impossible de bien programmer en C si
les bases ne sont pas solides.
Introduction à la programmation

La programmation est un sujet qui fascine énormément. Si vous lisez ce cours, c’est que vous avez décidé de franchir le pas et de découvrir ce que c’est que
programmer. Avant de commencer à apprendre quoi que ce soit sur le C et la programmation, il faudrait néanmoins se demander en quoi la programmation
consiste. En eﬀet, savez-vous réellement ce que c’est, comment programmer ou encore ce qui caractérise ce fameux langage C ? Ces questions importantes et
légitimes méritent des réponses. Ce chapitre va donc faire une introduction au monde de la programmation, et plus particulièrement au langage C.

 Avant-propos

Esprit et but du tutoriel
Ce tutoriel a été écrit dans un seul but : vous enseigner le langage C de la manière la plus complète, la plus rigoureuse et la plus instructive possible.
Pour ce faire, le tutoriel combinera beaucoup de théorie, de connaissances importantes, de détails et de recherches en profondeur avec de la pratique par
des exemples concrets, des exercices pratiques et des TP. Cette approche va réclamer de votre part des eﬀorts puisque le tutoriel semblera parfois complexe.
Nous avons choisi cette méthode d’apprentissage, car c’est celle que nous jugeons la plus payante. Elle s’oppose à une plus rapide, qui permet certes d’acquérir
des connaissances rapidement, mais qui s’avère bien souvent peu payante sur le long terme, beaucoup de programmeurs débutants étant ainsi perdus lorsqu’ils
sont jetés dans la jungle de la programmation à la sortie d’un cours, n’ayant pas acquis de bonnes pratiques et de la rigueur. Nous allons donc essayer de
vous enseigner non seulement un langage, mais aussi de bonnes pratiques et de la rigueur.
En résumé, ce tutoriel fera un juste mélange entre théorie, détails et recherches avec de la pratique et des exemples.

 À qui est destiné ce tutoriel ?

Le tutoriel a pour but d’être accessible à n’importe qui. Que vous soyez un programmeur expérimenté, un total débutant ou que vous vouliez réviser certaines
notions du C, vous êtes le bienvenu dans ce tutoriel. Les explications seront les plus claires possible aﬁn de rendre la lecture accessible à tous.
Cependant, il y a quelques conditions. Même si nous avons conçu le tutoriel pour être clairs, il vous faut plusieurs qualités pour arriver à tenir jusqu’au bout.

• De la motivation : ce tutoriel va présenter de nombreuses notions, souvent très théoriques, et qui sembleront parfois complexes. Il vous faut
donc être bien motivés pour proﬁter pleinement de cet apprentissage.

• De la logique : apprendre la programmation, c’est aussi être logique. Bien sûr, ce tutoriel vous apprendra à mieux l’être, mais il faut néanmoins
savoir réﬂéchir par soi-même et ne pas compter sur les autres (tutoriels ou forums) pour faire le travail à sa place.
• De la patience : vous vous apprêtez à apprendre un langage de programmation. Pour arriver à un sentiment de maitrise, il va falloir de la patience
pour apprendre, comprendre, s’entrainer, faire des erreurs et les corriger.
• De la rigueur : cette qualité, nous allons tenter de vous l’inculquer à travers ce cours. Elle est très importante, car c’est elle qui fera la diﬀérence
entre un bon et un mauvais programmeur.
• De la passion : le plus important pour suivre ce tutoriel, c’est de prendre plaisir à programmer. Amusez-vous en codant, c’est le meilleur moyen de
progresser !

Je tiens aussi à préciser qu’un niveau acceptable en anglais est très fortement recommandé. En eﬀet, beaucoup de cours, de forums, de documentations et
autres seront en anglais. Tôt ou tard, vous serez confronté à l’anglais, il faut vous y préparer. Si vous êtes encore étudiant, cela ne vous sera que bénéﬁque
! Si vraiment l’anglais n’est vraiment pas votre fort, vous pouvez utiliser un dictionnaire pour vous aider.
Dernier point qui concerne les mathématiques : contrairement à la croyance populaire, un bon niveau en maths n’est absolument pas nécessaire pour faire
de la programmation. Certes, ça peut aider en développant la logique, mais si les mathématiques ne sont pas votre fort, vous pourrez quand même suivre ce
cours sans problèmes.

Allez plus loin
Un des concepts fondamentaux de l’apprentissage de notions informatiques sur Internet est le croisement des sources. Il permet de voir la programmation sous
un angle diﬀérent. Par exemple, quelques cours de Developpez d’approches diﬀérentes sont à votre entière disposition. N’hésitez pas non plus à lire des
livres sur le C, notamment le K&R, écrit par les auteurs du C (une version traduite en français est disponible aux éditions Dunod). C’est un livre très
complet qui pourra vous être utile.
Enﬁn le plus important : n’hésitez pas à programmer tout seul. Faites des exercices, modiﬁez les codes du tutoriel, regardez ceux des autres, participez à
des projets. C’est la meilleure façon de progresser.

 La programmation, qu’est-ce que c’est ?

La programmation est une branche de l’informatique qui sert à créer des programmes. Tout ce que vous possédez sur votre ordinateur sont des programmes
: votre navigateur Internet (Internet Explorer, Firefox, Opera, etc.), votre sys tème d’exploitation (Windows, GNU/Linux, etc.) qui est un regroupement de
plusieurs programmes appelé logiciel, votre lecteur MP3, votre logiciel de discussion instantanée, vos jeux vidéos, etc.
Les programmes expliqués en long, en large et en travers

Un programme est une séquence d’instructions, d’ordres, donnés à l’ordinateur aﬁn qu’il exécute des actions. Ces instructions sont généralement assez basiques.
On trouve ainsi des instructions d’addition, de multiplication, ou d’autres opérations mathématiques de base, qui font que notre ordinateur est une vraie ma-
chine à calculer. D’autres instructions plus complexes peuvent exister, comme des opérations permettant de comparer des valeurs, traiter des caractères, etc.
Créer un programme, c’est tout simplement créer une suite d’instructions de base qui permettra de faire ce que l’on veut. Tous les programmes sont créés
ainsi : votre lecteur MP3 donne des instructions à l’ordinateur pour écouter de la musique, le chat donne des instructions pour discuter avec d’autres gens sur le
réseau, le système d’exploitation donne des instructions pour dire à l’ordinateur comment utiliser le matériel et comment fonctionner, etc.
Petite remarque : on ne peut pas créer d’instructions. Notre ordinateur est conçu, câblé, et peut traiter certaines instructions de bases, précâblées dans ses
circuits, sans possibilité d’en inventer d’autres (sauf cas particulier vraiment tordus). Notre ordinateur contient un composant électronique particulier, spé-
cialement conçu pour eﬀectuer ces instructions : il s’agit du processeur. Ce qu’il faut retenir, c’est que notre ordinateur contient un circuit, le processeur,
qui permet d’eﬀectuer de petits traitements de base qu’on appelle instructions et qui sont la base de tout ce qu’on trouve sur un ordinateur.
Pour les curieux, il existe un cours sur le fonctionnement d’un ordinateur expliqué depuis zéro.
Ces instructions sont stockées dans notre ordinateur sous la forme de bits, de petites données qui valent soit 0, soit 1. Ainsi, nos instructions ne sont rien
d’autre que des suites de 0 et de 1, stockées dans notre ordinateur, et que notre processeur va interpréter comme étant des ordres à eﬀectuer. Ces suites de
zéros et un sont diﬃcilement compréhensibles pour nous humains, et parler à l’ordinateur avec des 0 et des 1 est très dur et très long. Autant vous dire que
créer des programmes de cette façon revient à se tirer une balle dans le pied.
Pour vous donner un exemple, imaginez que vous devez communiquer avec un étranger alors que vous ne connaissez pas sa langue. Communiquer avec un
ordinateur reviendrait à devoir lui donner une suite de 0 et de 1 : ça risque de prendre énormément de temps et cela serait diﬃcile. Tout se passe comme
si votre processeur parlait un langage particulier, composé de suite de zéro et d’un bien organisé, et qu’il était incapable de parler autre chose. Le langage
du processeur s’appelle le langage machine. Une question doit vous venir à l’esprit : comment communiquer avec notre processeur sans avoir à apprendre
sa langue ?
L’idéal serait de parler à notre processeur en français, en anglais, etc. Mais disons-le clairement : notre technologie n’est pas suﬃsamment évoluée, et nous
avons dû trouver autre chose. La solution retenue a été de créer des langages de programmation plus évolués que le langage du processeur, plus faciles à appren-
dre, et de fournir le traducteur qui va avec. Ces langages de programmation plus évolués sont des sortes de langages assez simpliﬁés, assez proches des lan-
gages naturels, et dans lesquels on peut écrire nos programmes beaucoup plus simplement qu’en utilisant le langage machine. Grâce à eux, on peut écrire
nos programmes sous forme de texte, sans avoir à se débrouiller avec des suites de 0 et de 1 totalement incompréhensibles. Il existe de nombreux langages de
programmation, et le C est un de ces langages.
Reste que notre processeur ne comprend pas ces langages évolués ; il ne comprend qu’un seul langage, le sien. Pour utiliser nos langages de programmation,
il faut aussi avoir une sorte de traducteur qui fera le lien entre votre langage de programmation et le langage machine du processeur. Ce traducteur va ainsi
traduire du texte (écrit dans un langage de programmation évolué) en une suite de zéro et d’un que le processeur peut comprendre. Ainsi vous pourrez com-
mander votre processeur même si vous ne parlez pas sa langue.
Pour illustrer, ce code écrit en C (que nous apprendrons à connaître) est quand même largement plus facile à comprendre qu’une suite de 0 et de 1.
“‘c #include
int main(void) { printf(“Salut !”); return 0; } “‘ Imaginez la même chose composée de 0 et de 1,
et vous comprendrez tout l’intérêt d’un langage de programmation.
Il ne reste plus qu’à utiliser un interprète qui va traduire ce texte (un programme écrit dans notre langage de programmation) vers la langue de l’ordinateur (des
suites de 0 et de 1) : le compilateur. Voici un petit schéma qui résume tout ça :

Figure 1: Schéma simpliﬁé de la compilation
Le langage C

Malgré tous ces langages de programmation disponibles, nous allons dans ce tutoriel nous concentrer sur un seul langage : le langage C. Avant de parler des
caractéristiques de ce langage et des choix qui nous amènent à l’étudier dans ce cours, faisons un peu d’histoire.
L’histoire du C

Le langage C est né au début des années 1970 dans les laboratoires AT&T aux États-Unis. Son concepteur, Dennis Ritchie, souhaitait améliorer un langage
existant, le B, aﬁn de lui adjoindre des nouveautés. En 1973, le C était pratiquement au point, et il commença à être distribué l’année suivante. Son succès
était tel auprès des informaticiens que l’ANSI en 1989, puis l’ISO en 1990 décidèrent de le normaliser, c’est-à-dire d’établir les règles oﬃcielles du langage.
On parle donc de C89 / C ANSI ou bien C90 / C ISO (au choix). D’autres normes sortirent plus tard, en 1999 (on parle de C99) et en 2011 (on parle de C11).
Si vous voulez en savoir plus sur l’histoire du C, lisez donc ce tutoriel.

 Pourquoi apprendre le C ?

C’est une très bonne question. Après tout, il existe tellement de langages dif férents, et on peut logiquement se demander pourquoi le C en particulier ? Il y
a plusieurs raisons à ça.

• Sa popularité : il fait partie des langages de programmation les plus utilisés. Il possède une communauté très importante et de nombreux
tutoriels et documentations. Vous aurez donc toujours du monde pour vous aider. De plus, il existe beaucoup de programmes et de bibliothèques
développés en et pour le C.

• Sa rapidité : le C est connu pour être un langage très rapide, ce qui en fait un langage de choix pour tout programme où la vitesse est cruciale.
• Sa légèreté : le C est léger, ce qui le rend utile pour les programmes embarqués où la mémoire disponible est faible.
• Sa portabilité : cela veut dire qu’un programme développé en C marche théoriquement sur n’importe quelle plateforme. Il faut savoir que le C a été
conçu pour la programmation système (drivers, systèmes d’exploitation, matériel embarqué, etc). Or, les plate-formes étant diﬀérents, il était dif-
ﬁcile à l’époque d’avoir un code générique pouvant marcher sur n’importe quel environnement. La volonté des créateurs du C était donc de faire un
langage permettant de produire du code portable.

Ce ne sont que quelques raisons, mais elles sont à mon gout suﬃsantes pour apprendre ce langage. Bien entendu, le C comporte aussi sa part de défauts. On
peut citer la tolérance aux comportements dangereux qui fait que le C demandebeaucoup de rigueur pour ne pas tomber dans certains « pièges », un nombre
plus restreint de concepts (c’est parfois un désavantage, car on est alors obligé
de recoder certains mécanismes qui existent nativement dans d’autres langages), etc. D’ailleurs, si votre but est de développer rapidement des programmes amu-
sants, le C n’est pas du tout adapté pour ça, et je vous encourage à vous tourner vers d’autres langages comme le Python par exemple.
Le C possède aussi une caractéristique qui est à la fois un avantage et un défaut: c’est un langage plutôt de bas niveau. Cela veut dire qu’il permet de pro-
grammer en étant proche de sa machine, en cherchant à vraiment comprendre ce que l’on fait. C’est à double tranchant : c’est plus diﬃcile et plus long, mais
on en apprend beaucoup sur sa machine et on a un grand contrôle de ce que l’on fait. Cette notion de bas niveau est d’ailleurs à opposer aux langages de
haut niveau, qui permettent de programmer en faisant abstraction d’un certain nombre de choses. Le développement est souvent plus facile et plus rapide,
mais en contrepartie on voit moins bien le fonctionnement de la machine. Ces notions de haut et bas niveau sont néanmoins à nuancer, car elles dépendent du
langage utilisé et du point de vue du programmeur.
Je termine cette partie en rajoutant quelque chose. Peut-être avez-vous entendu parler du C++. C’est un langage de programmation qui a été inventé dans les
années 1980 par Bjarne Stroustrup, un collègue de Dennis Ritchie, qui souhaitait rajouter des éléments au C. Bien que très ressemblants à l’époque de sa création,
ces deux langages sont aujourd’hui très diﬀérents (on ne programme pas et on ne réﬂéchit pas de la même façon en C qu’en C++). Ne croyez pas qu’il y a un
langage meilleur que l’autre. Ils sont simplement diﬀérents. Si d’ailleurs votre but est d’apprendre le C++, je vous encourage à le faire. Contrairement à ce
que l’on pense et dit souvent, il n’y a pas besoin de connaitre le C pour ça. Ce tutoriel ne se concentrera quant à lui que sur ce dernier.

 La norme

Comme précisé plus haut, le C est un langage qui possède des règles. Ces règles ont été déﬁnies par des informaticiens professionnels et sont toutes regroupées
dans ce que l’on appelle la norme du langage. Cette norme sert de référence à tous les programmeurs. Chaque fois que l’on a un doute ou que l’on se pose une
question, le premier réﬂexe est de regarder dans la norme ce qui est dit. Bien entendu, la norme n’est pas parfaite et ne répond pas à toutes les questions, et ne
précise pas tous les détails. Néanmoins, elle reste la référence du programmeur.
Cette norme sert aussi de référence pour les compilateurs. En eﬀet, tous les compilateurs respectent cette norme (en règle générale), ce qui fait qu’il n’y aura
pas diﬀérentes interprétations d’un même code. Cette norme est l’équivalent des règles d’orthographe, de grammaire et de conjugaison de nos interprètes.
Imaginez si chacun écrivait ou conjuguait à sa guise tout ce qu’il veut. La norme sert donc à oﬃcialiser tout un tas de règles pour que tous les interprètes
(et donc les compilateurs) la suivent.
Il existe plusieurs versions de la norme : le C89, le C99 et le C11. Dans ce cours, nous avons décidé de nous servir de la norme C89. En eﬀet, même si c’est la
plus ancienne et qu’elle semble restrictive à certains, elle permet néanmoins de développer avec n’importe quel compilateur sans problèmes, contrairement aux
normes C99 et C11 que tous les compilateurs ne connaissent pas. De plus, il est très facile de passer aux normes plus récentes ensuite. Voici le lien vers le
brouillon de cette norme. Cela signiﬁe que ce n’est pas la version déﬁnitive et oﬃcielle de la norme, celle-ci est très chère à obtenir, alors que le brouillon est
largement suﬃsant pour notre niveau et gratuit. Bien entendu, cette norme est en anglais.
[ANSI]: American National Standard Institute[ISO]: International Standard Organisation

 L’algorithmique

L’algorithmique est très liée à la programmation, et elle constitue même une branche à part des mathématiques.
Elle consiste à déﬁnir et établir des algorithmes.
Un algorithme peut se déﬁnir comme étant une suite ﬁnie et non-ambiguë d’opérations permettant de résoudre un problème. En clair, il s’agit de calculs
qui prennent plusieurs paramètres et fournissent un résultat. Les algorithmes ne sont pas limités à l’informatique, ils existaient même avant son apparition ;
prenez les recettes de cuisine par exemple, ou des instructions de montage d’un meuble ou d’un Lego : ce sont des algorithmes.
L’intérêt principal des algorithmes est qu’ils sont très utiles lorsqu’ils sont en relation avec des ordinateurs. En eﬀet, ces derniers peuvent exécuter des mil-
liards d’instructions à la seconde, ce qui les rend bien plus rapides qu’un humain.
Illustrons : imaginez que vous deviez trier une liste de 10 nombres dans l’ordre croissant. C’est assez facile et faisable en quelques secondes. Et pour plusieurs
milliards de nombres ? C’est impossible pour un humain, alors qu’un ordinateur le fera rapidement.
Ce qu’il faut retenir, c’est qu’un algorithme est une suite d’opérations destinéeà résoudre un problème donné. Nous aurons l’occasion d’utiliser quelques al-
gorithmes dans ce cours, mais nous ne nous concentrerons pas dessus. Si vous voulez en savoir plus, lisez le tutoriel sur l’algorithmique pour l’apprenti pro-
grammeur en même temps que vous apprenez à programmer avec celui-ci.

 Le pseudo-code

Pour représenter un algorithme indépendamment de tout langage, on utilise ce qu’on appelle un pseudo-code. Il s’agit de la description des étapes de
l’algorithme en langage naturel (dans notre cas le français). Voici un exemple de pseudo-code :
Fonction max (x, y)

Si x est supérieur à y
Retourner x
Sinon
Retourner y

Fin fonction

Dans ce cours, il y aura plusieurs exercices dans lesquels un algorithme fourni devra être implémenté (traduit) en C. Si vous voulez vous entrainer davantage
tout en suivant ce cours, je vous conseille France-IOI qui permet d’implémenter divers algorithmes en plusieurs langages dont le C.
Cela pourra être un excellent complément.
Comme vous avez pu le constater, la programmation est un monde vaste, très vaste, et assez complexe. Comme il existe une multitude de langages de pro-
grammation, il faut se concentrer sur un seul d’entre eux à la fois. Dans notre cas, il s’agit du C. Ce langage, et retenez-le bien, est à la fois très puissant et com-
plexe. Souvenez-vous bien qu’il vous faudra faire des eﬀorts pour l’apprendre correctement.
Si vous vous sentez prêts, alors rendez-vous dans le chapitre suivant, qui vous montrera les outils utilisés par un programmeur en C.

Outils

Maintenant que les présentations sont faites, il est temps de découvrir les outils nécessaires pour programmer en C.
Le strict minimum pour programmer se résume en trois points.

• Un éditeur de texte : ce logiciel va servir à écrire le code source. En théorie, n’importe quel éditeur de texte suﬃt, mais le mieux est d’en
avoir qui colore le code source, ce qui permet une relecture plus agréable.

• Un compilateur : c’est le logiciel le plus important puisqu’il va nous permettre de transformer le code que l’on écrit en un ﬁchier exécutable
compréhensible par le processeur.
• Un débugger / débogueur (prononcez « débegueur ») : fondamentalement, il n’est pas indispensable, mais ce logiciel est très utile pour chasser
les bugs et vériﬁer le comportement de son programme.

À partir de là, il existe deux moyens de récupérer tous ces logiciels : soit on les prend séparément, et dans ce cas il faut compiler par soi-même, soit on utilise
un logiciel qui réunit les trois : un IDE (EDI en français).
Face à la multitude de logiciels diﬀérents qui existent, ce chapitre a pour but de vous guider en vous montrant quelques logiciels, que ce soit pour compiler à la
main ou avec un IDE.
[IDE]: Integrated Development Environment[EDI]: Environnement de Développement Intégré

 Windows

Bien que de nombreux IDE soient disponibles pour Windows, nous ne parlerons que de deux d’entre eux : Code::Blocks et Visual C++, sans oublier une partie
consacrée à la compilation ‘’via” l’invite de commande.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\00. TABLE DES MATIERES.rtf

########################### TABLE DES MATIERES ############################
LE LANGAGE C Vu dans ......................... LE_C.zip 318 ko (PDF 384_Ko)
Pas de d'auteurs ni de marque dans le PDF C.zip 318 ko

 1. GÉNÉRALITÉS SUR LE LANGAGE C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1.1   PRÉSENTATION PAR L'EXEMPLE DE QUELQUES INSTRUCTIONS DU LANGAGE C .. 1
1.1.1 Un exemple de programme en langage C .............................. 1
1.1.2 Structure d'un programme en langage C.............................. 1
1.1.3 Déclarations ...................................................... 1
1.1.4 Pour écrire des informations: la fonction printf .................. 1
1.1.5 La fonction return ................................................ 2
1.1.6 Les directives à destination du préprocesseur ..................... 2
1.2   QUELQUES RÈGLES D'ÉCRITURE ........................................ 2
1.2.1 Les identificateurs ............................................... 2
1.2.2 Les mots clés ..................................................... 2
1.2.3 Les séparateurs ................................................... 2
1.2.4 Les commentaires .................................................. 3
1.3   CRÉATION D'UN PROGRAMME EN LANGAGE C .............................. 3
1.3.1 L'édition du programme ............................................ 3
1.3.2 La compilation .................................................... 3
1.3.3 L'édition de liens ................................................ 3

 2. LES TYPES DE BASES DU LANGAGE C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2.1   LA NOTION DE TYPE ................................................. 4
2.2   LES TYPES ENTIERS ................................................. 4
2.2.1 Leur représentation en mémoire .................................... 4
2.2.2 Les différents types d'entiers .................................... 4
2.2.3 Notation des constantes entières .................................. 5
2.3   LES TYPES FLOTTANTS ............................................... 5
2.3.1 Les différents types et leur représentation en mémoire ............ 5
2.3.2 Notation des constantes flottantes ................................ 5
2.4   LES TYPES CARACTÈRES .............................................. 6
2.4.1 La notion de caractère en langage C ............................... 6
2.4.2 Notation des constantes caractères ................................ 6
2.5   INITIALISATION ET CONSTANTES ...................................... 6

 3. LES OPÉRATEURS ET LES EXPRESSIONS EN LANGAGE C
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.1   L'ORIGINALITÉ DES NOTIONS D'OPÉRATEUR ET D'EXPRESSION EN LANGAGE C. 7
3.2   LES OPÉRATEURS ARITHMÉTIQUES EN C ................................. 7
3.2.1 Présentation des opérateurs ....................................... 7
3.2.2 Les priorités relatives des opérateurs ............................ 7
3.2.3 Le cas du type char ............................................... 7
3.3   LES OPÉRATEURS RELATIONNELS ....................................... 7
3.4   LES OPÉRATEURS LOGIQUES ........................................... 8
3.5   L'OPÉRATEUR D'AFFECTATION ORDINAIRE ............................... 8
3.6   LES OPÉRATEURS D'INCRÉMENTATION ET DE DÉCRÉMENTATION .............. 8
3.6.1 Leur rôle ......................................................... 8
3.6.2 Leurs priorités ................................................... 9
3.6.3 Leur intérêt ...................................................... 9
3.7   LES OPÉRATEURS D'AFFECTATION ÉLARGIE ............................. 10

 4. LES ENTRÉES-SORTIES CONVERSATIONELLES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4.1   LES POSSIBILITÉS DE LA FONCTION PRINTF ........................... 11
4.1.1 Les principaux codes de conversion ............................... 11
4.1.2 Action sur le gabarit d'affichage ................................ 11
4.1.3 La macro putchar ................................................. 11
4.2   LES POSSIBILITÉS DE LA FONCTION SCANF ............................ 12
4.2.1 Les principaux codes de conversion de scanf ...................... 12
4.2.2 La macro getchar ................................................. 12
4.3   Exercice : ....................................................... 13

 5. LES INSTRUCTIONS DE CONTROLES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5.1   L'INSTRUCTION IF ................................................. 14
5.1.1 Blocs d'instructions ............................................. 14
5.1.2 Syntaxe de l'instruction if ...................................... 14
5.2   L'instruction switch ............................................. 14
5.3   L’instruction do... while ........................................ 15
5.4   L’instruction while .............................................. 15
5.5   L'INSTRUCTION FOR ................................................ 15
5.5.1 Exemple d'introduction de l'instruction for ...................... 15
5.5.2 Syntaxe de l’instruction FOR ..................................... 16
5.6   Exercices : ...................................................... 16

 6. LA PROGRAMMATION MODULAIRE ET LES FONCTIONS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6.1   LA FONCTION: LA SEULE SORTE DE MODULE EXISTANT EN C .............. 17
6.2   EXEMPLE DE DÉFINITION ET D'UTILISATION D'UNE FONCTION EN C ....... 17
6.3   QUELQUES RÈGLES .................................................. 19
6.3.1 Arguments muets et arguments effectifs ........................... 19
6.3.2 L'instruction return ............................................. 19
6.3.3 Cas des fonctions sans valeur de retour ou sans arguments ........ 19
6.4   EN C, LES ARGUMENTS SONT TRANSMIS PAR VALEUR ..................... 20
6.5   LES VARIABLES GLOBALES ........................................... 21
6.6   LES VARIABLES LOCALES ............................................ 21
6.7   LES FONCTIONS RECURSIVES ......................................... 21
6.8   Exercices ........................................................ 21

 7. LES TABLEAUX ET LES POINTEURS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7.1   LES TABLEAUX A UN INDICE ......................................... 23
7.1.1 Exemple d'utilisation d'un tableau en C .......................... 23
7.1.2 Quelques règles .................................................. 24
7.1.3 LES TABLEAUX À PLUSIEURS INDICES ................................. 24
7.1.4 Leur déclaration ................................................. 24
7.2   INITIALISATION DES TABLEAUX ...................................... 25
7.2.1 NOTION DE POINTEUR - LES OPÉRATEURS * ET & ....................... 25
7.2.2 Introduction ..................................................... 25
7.2.3 Quelques exemples ................................................ 25
7.2.4 Incrémentation de pointeurs ...................................... 26
7.3   COMMENT SIMULER UNE TRANSMISSION PAR ADRESSE AVEC UN POINTEUR .... 27
7.4   UN NOM DE TABLEAU EST UN POINTEUR CONSTANT ....................... 27
7.4.1 Cas des tableaux à un indice ..................................... 27
7.4.2 Cas des tableaux à plusieurs indices ............................. 28
7.5   LES TABLEAUX TRANSMIS EN ARGUMENT ................................ 29
7.5.1 Cas des tableaux à un indice ..................................... 29
7.5.2 Cas des tableaux à plusieurs indices ............................. 29
7.6   Exercices ........................................................ 30

 8. LES CHAINES DE CARACTÈRES
~~~~~~~~~~~~~~~~~~~~~~~~~~
8.1   REPRÉSENTATiON DES CHAINES ....................................... 31
8.1.1 La convention adoptée ............................................ 31
8.1.2 Cas des chaînes constantes ....................................... 31
8.1.3 Initialisation de tableaux de caractères ......................... 31
8.1.4 Initialisation de tableaux de pointeurs sur des chaînes .......... 32
8.2   POUR LIRE ET ÉCRIRE DES CHAINES .................................. 32
8.3   GÉNÉRALITÉS SUR LES FONCTIONS PORTANT SUR DES CHAINES ............ 33
8.4   Exercices ........................................................ 34

 9. LES STRUCTURES
~~~~~~~~~~~~~~
9.1   DÉCLARATION D'UNE STRUCTURE ...................................... 35
9.2   UTILISATION D'UNE STRUCTURE ...................................... 35
9.2.1 Utilisation des champs d'une structure ........................... 35
9.2.2 Utilisation globale d'une structure .............................. 36
9.2.3 Initialisations de structures .................................... 36
9.3   POUR SIMPLIFIER LA DÉCLARATION DE TYPES DÉFINIR DES SYNONYMES AVEC
      TYPEDEF .......................................................... 36
9.3.1 Exemples d'utilisation de typedef ................................ 36
9.3.2 Application aux structures ....................................... 37
9.4   IMBRICATION DE STRUCTURES ........................................ 37
9.4.1 Structure comportant des tableaux ................................ 37
9.4.2 Tableaux de structures ........................................... 38
9.5   À PROPOS DE LA PORTÉE DU MODÈLE DE STRUCTURE ..................... 38
9.6   TRANSMISSION D'UNE STRUCTURE EN ARGUMENT D'UNE FONCTION .......... 39
9.6.1 Transmission de la valeur d'une structure ........................ 39
9.6.2 Transmission de l'adresse d'une structure: l'opérateur -> ........ 39
9.7   Exercices ........................................................ 40

 10. LES FICHIERS
~~~~~~~~~~~~
10.1   CRÉATION SÉQUENTIELLE D'UN FICHIER .............................. 41
10.2   LISTE SÉQUENTIELLE D'UN FICHIER ................................. 42
10.3   L'ACCÈS DIRECT .................................................. 43
10.3.1 Accès direct en lecture sur un fichier existant ................. 44
10.3.2 Les possibilités de l'accès direct .............................. 44
10.3.3 En cas d'erreur ................................................. 45
10.4   LES ENTRÉES-SORTIES FORMATÉES ET LES FICHIERS DE TEXTE .......... 46
10.5   LES DIFFÉRENTES POSSIBILITÉS D'OUVERTURE D'UN FICHIER ........... 47
10.6   LES FICHIERS PRÉDÉFINIS ......................................... 48
10.7   Exercices ....................................................... 48

 11. LA GESTION DYNAMIQUE DE LA MEMOIRE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11.1   LES OUTILS DE BASE DE LA GESTION DYNAMIQUE: MALLOC ET FREE ...... 50
11.1.1 La fonction malloc .............................................. 50
11.1.2 La fonction free ................................................ 51
11.2   Exercice : utiliser ces nouveautés pour compléter votre programme
       sur les fichiers ................................................ 51

12     Table des matiéres ..................................... 52 53 54 55

 ###### LE LANGAGE C ########## Vu dans ... LE_C.zip 318 ko (via PDF) ######


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\01. GÉNÉRALITÉS SUR LE LANGAGE C.rtf

1. GÉNÉRALITÉS SUR LE LANGAGE C
Dans ce chapitre, nous vous proposons une première approche d'un prgm en C.
Nous dégagerons ensuite quelques règles générales concernant l'écriture
d'un programme. Enfin, nous vous montrerons comment s'organise le dévelop-
pement d'un programme en vous rappelant ce que sont: l'édition, la compila-
tion, l'édition de liens et l'exécution.

 1.1 PRÉSENTATION PAR L'EXEMPLE DE QUELQUES INSTRUCTIONS DU LANGAGE C
1.1.1 Un exemple de programme en langage C
Voici un premier exemple très simple de programme en langage C.

#include <stdio.h>
int main(void)
{
int i ;
double x ;
i=0 ;
x=3.1415926535
printf(‘’Bonjour tout le monde \n‘’) ;
printf(‘’i vaut : %d et x vaut :%f’’,i,x) ;
return 0 ;
}

 1.1.2 Structure d'un programme en langage C
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La ligne: int main(void ) se nomme un "en-tête". Elle précise que ce qui
sera décrit à sa suite est en fait le ''programme principal" (int main).
Lorsque nous aborderons l'écriture des fonctions en C, nous verrons que
celles-ci possèdent également un tel en-tête; ainsi, en C, le programme
principal apparaîtra en fait comme une fonction dont le nom (int main)
est imposé.
Le programme (principal) proprement dit vient à la suite de cet en-tête.
Il est délimité par les accolades "{" et "}". On dit que les instructions
situées entre ces accolades forment un "bloc". Ainsi peut-on dire que la
fonction main est constituée d'un en-tête et d'un bloc; il en ira de même
pour toute fonction C. Notez qu'un bloc (comme en Pascal) peut lui-même
contenir d'autres blocs. En revanche, nous verrons qu'une fonction ne peut
jamais contenir d'autres fonctions (ce qui est le cas du Pascal).

 1.1.3 Déclarations
 ~~~~~~~~~~~~~~~~~~
Les deux instructions : int i; double x; sont des "déclarations".
- La première précise que la variable nommée i est de type int,c'est-à-dire
qu'elle est destinée à contenir des nombres entiers (relatifs).
Nous verrons qu'en C il existe plusieurs types d'entiers.
- L’autre déclaration précise que la variables x sont de type double
c'est-à-dire qu'elles sont destinées à contenir des nombres flottants
(approximation de nombres réels).
Là encore, nous verrons qu'en C il existe plusieurs types flottants.
En C, comme en Pascal, les déclarations des types des variables sont obli-
gatoires et doivent être regroupées au début du programme
(on devrait plutôt dire: au début de la fonction main).
Il en ira de même pour toutes les variables définies dans une fonction;on
les appelle "variables locales" (en toute rigueur,les variables définies
dans notre exemple sont des variables locales de la fonction main).
Nous verrons également (dans le chapitre consacré aux fonctions) qu'on
peut définir des variables en dehors de toute fonction;
on parlera alors de variables globales.

 1.1.4 Pour écrire des informations: la fonction printf
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le langage C 2
L'instruction : printf ("Bonjour\n") ; appelle en fait une fonction
"prédéfinie" (fournie avec le langage, et donc que vous n'avez pas à
écrire vous-même) nommée printf. Ici, cette fonction reçoit un argument
qui est : "Bonjour\n"
Les guillemets servent à délimiter une "chaîne de caractères"
(suite de caractères). La notation \n est conventionnelle: elle représente
un caractère de fin de ligne, c'est-à-dire un caractère qui, lorsqu'il est
envoyé à l'écran, provoque le passage à la ligne suivante.Nous verrons que,
de manière générale, le langage C prévoit une notation de ce type
(\ suivi d'un caractère) pour un certain nombre de caractères dits
"de contrôle", c'est-à-dire ne possédant pas de "graphisme" particulier.
Notez que, apparemment, bien que printf soit une fonction, nous n'utilisons
pas sa valeur. Nous aurons l'occasion de revenir sur ce point, propre au
langage C. Pour l'instant, admettez que nous pouvons, en C, utiliser une
fonction comme ce que d'autres langages nomment une "procédure" ou un
"sous-programme".
1.1.5 La fonction return
~~~~~~~~~~~~~~~~~~~~~~~~
On la reverra plus tard, notez simplement qu’elle est obligatoire.

 1.1.6 Les directives à destination du préprocesseur
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La première ligne de notre programme : #include <stdio.h> est en fait un
peu particulière. Il s'agit d’une "directive" qui est prise en compte
avant la traduction (compilation) du programme. Ces directives,
contrairement au reste du programme, doivent être écrites à raison
d'une par ligne et elles doivent obligatoirement commencer en début de
ligne. Leur emplacement au sein du programme n'est soumis à aucune
contrainte (mais une directive ne s'applique qu'à la partie du programme
qui lui succède). D'une manière générale, il est préférable de les placer
au début, comme nous l'avons fait ici. La directive demande en fait
d'introduire (avant compilation) des instructions (en langage C) situées
dans le fichier stdio.h. Notez qu'un même fichier en-tête contient des
déclarations relatives à plusieurs fonctions. En général, il est indis-
pensable d'incorporer stdio.h.

 1.2 QUELQUES RÈGLES D'ÉCRITURE
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ce paragraphe vous expose un certain nombre de règles générales intervenant
dans l'écriture d'un programme en langage C. Nous y parlerons précisément
de ce que l'on appelle les "identificateurs" et les "mots clés", du format
libre dans lequel on écrit les instructions, de l'usage des séparateurs
et des commentaires.

 1.2.1 Les identificateurs
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
Les identificateurs servent à désigner les différents "objets" manipulés
par le programme: variables, fonctions, etc Comme dans la plupart des
langages, ils sont formés d'une suite de caractères choisis parmi les
lettres ou les chiffres,le premier étant nécessairement une lettre.

 1.2.2 Les mots clés
 ~~~~~~~~~~~~~~~~~~~
Certains "mots clés" sont réservés par le langage à un usage bien défini
et ne peuvent pas être utilisés comme identificateurs.En voici la liste,
classée par ordre alphabétique.

auto default float register struct volatile break do for
return switch while case double goto short typedef char
else if signed union const enum int sizeof
unsigned continue extern long static void

 1.2.3 Les séparateurs
 ~~~~~~~~~~~~~~~~~~~~~~
Dans notre langue écrite, les différents mots sont séparés par un espace,
un signe de ponctuation ou une fin de ligne. Il en va quasiment de même
en langage C dans lequel les règles vont donc paraître naturelles.
Ainsi, dans un programme,deux identificateurs successifs entre lesquels
la syntaxe n'impose aucun signe particulier doivent impérativement être
séparés soit par un espace, soit par une fin de ligne. Par contre, dès
que la syntaxe impose un ....
.... séparateur quelconque, il n'est alors pas nécessaire de prévoir
d'espaces supplémentaires (bien qu'en pratique cela améliore la
lisibilité du programme).
Ainsi, vous devrez impérativement écrire : int x,y et non : intx,y
En revanche, vous pourrez écrire indifféremment : int n,compte,total
ou plus lisiblement : int n, compte, total

 1.2.4 Les commentaires
 ~~~~~~~~~~~~~~~~~~~~~~
Comme tout langage évolué,le langage C autorise la présence de commentaires
dans vos programmes source. Il s'agit de textes explicatifs destinés aux
lecteurs du programme et qui n'ont aucune incidence sur sa compilation.
Ils sont formés de caractères quelconques placés entre les symboles
/* et */ . Ils peuvent apparaître à tout endroit du programme où un
espace est autorisé. En général, cependant, on se limitera à des
emplacements propices à une bonne lisibilité du programme.
Voici quelques exemples de commentaires:

// programme de calcul de racines carrées

/* commentaire s'étendant sur plusieurs
lignes de programme source ......... */

 1.3 CRÉATION D'UN PROGRAMME EN LANGAGE C
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La manière de développer et d'utiliser un programme en langage C dépend
naturellement de l’environnement de programmation dans lequel vous
travaillez. Nous vous fournissons ici quelques indications générales
(s'appliquant à n'importe quel environnement) concernant ce que l'on
pourrait appeler les grandes étapes de la création d'un programme, à
savoir: édition, compilation et édition de liens.

 1.3.1 L'édition du programme
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
L'édition du programme (on dit aussi parfois "saisie") consiste à créer,
à partir d'un clavier, tout ou partie du texte d'un programme: on parle
alors de "programme source". En général, ce texte sera conservé dans un
fichier que l'on nommera "fichier source".

 1.3.2 La compilation
 ~~~~~~~~~~~~~~~~~~~~
Elle consiste à traduire le programme source (ou le contenu d'un fichier
source) en langage machine, en faisant appel à un programme nommé
compilateur. En langage C,compte tenu de l'existence d'un préprocesseur,
cette opération de compilation comporte en fait deux étapes.
Le résultat de compilation porte le nom de module objet.

 1.3.3 L'édition de liens
 ~~~~~~~~~~~~~~~~~~~~~~~~
Le module objet créé par le compilateur n'est pas directement exécutable.
Il lui manque, au moins, les différents modules objet correspondant aux
fonctions prédéfinies (on dit aussi "fonctions standard") utilisées par
votre programme (comme printf, scanf, sqrt).
C'est effectivement le rôle de l'éditeur de liens que d'aller rechercher
dans la "bibliothèque standard" les modules objet nécessaires.
Le résultat de l'édition de liens est ce que l'on nomme un "programme
exécutable", c'est-à-dire un ensemble autonome d'instructions en langage
machine. Si ce programme exécutable est rangé dans un fichier, il pourra
ultérieurement être exécuté sans qu'il soit nécessaire de faire appel à
un quelconque composant de l'environnement de programmation en C.
Exercice :
Ecrire un programme permettant d’afficher « bonjour » à l’écran.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\02. LES TYPES DE BASES DU LANGAGE C.rtf

2. LES TYPES DE BASES DU LANGAGE C
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les types char, int et double que nous avons déjà rencontrés sont souvent
dits "scalaires" ou "simples", car, à un instant donné, une variable
d'un tel type contient une seule valeur.
Ils s'opposent aux types "structurés" (on dit aussi "agrégés") qui
correspondent à des variables qui, à un instant donné, contiennent
plusieurs valeurs (de même type ou non).

 2.1 LA NOTION DE TYPE
 ~~~~~~~~~~~~~~~~~~~~~
La mémoire centrale est un ensemble de "positions binaires" nommées bits.
Les bits sont regroupés en octets (8 bits), et chaque octet est repéré par
ce qu'on nomme son adresse.
L'ordinateur, compte tenu de sa technologie (actuelle !),ne sait représenter
et traiter que des informations exprimées sous forme binaire.
Toute information, quelle que soit sa nature, devra être codée sous cette
forme. Dans ces conditions, on voit qu'il ne suffit pas de connaître le
contenu d'un emplacement de la mémoire (d'un ou de plusieurs octets) pour
être en mesure de lui attribuer une signification. Par exemple, si "vous"
savez qu'un octet contient le "motif binaire" suivant : 01001101 vous
pouvez considérer que cela représente le nombre entier 77 (puisque le
motif ci-dessus correspond à la représentation en base 2 de ce nombre).
Mais pourquoi cela représenterait-il un nombre? En effet, toutes les
informations (nombres entiers, nombres réels, nombres complexes,caractères,
instructions de programme en langage machine, graphiques...) devront,
au bout du compte, être codées en binaire.
Dans ces conditions,les huit bits ci-dessus peuvent peut-être représenter
un caractère; dans ce cas, si nous connaissons la convention employée sur
la machine concernée pour représenter les caractères,nous pouvons lui faire
correspondre un caractère donné (par exemple M, dans le cas du code ASCII).
On comprend donc qu'il n'est pas possible d'attribuer une signification à
une information binaire tant que l'on ne connaît pas la manière dont elle
a été codée. Qui plus est, en général, il ne sera même pas possible de
"traiter" cette information. Par exemple, pour additionner deux
informations, il faudra savoir quel codage a été employé afin de pouvoir
mettre en œuvre les "bonnes" instructions (en langage machine).
D'une manière générale, la notion de type, telle qu'elle existe dans les
langages évolués, sert à régler (entre autres choses) les problèmes que
nous venons d'évoquer.
Les types de base du langage C se répartissent en trois grandes catégories
en fonction de la nature des informations qu'ils permettent de représenter:
- nombres entiers (mot clé int),
- nombres flottants (mot clé double),
- caractères (mot clé char);
nous verrons qu'un char apparaît (en C) comme un cas particulier de int.

 2.2 LES TYPES ENTIERS
 ~~~~~~~~~~~~~~~~~~~~~~
 2.2.1 Leur représentation en mémoire
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le mot clé int correspond à la représentation de nombres entiers relatifs.
Pour ce faire: un bit est réservé pour représenter le signe du nombre
(en général 0 correspond à un nombre positif); les autres bits servent
à représenter la valeur absolue du nombre.

 2.2.2 Les différents types d'entiers
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le C prévoit que, sur une machine donnée,on puisse trouver jusqu'à trois
"tailles" différentes d'entiers, désignées par les mots clés suivants:

- short
- int (c'est celui que nous utiliserons systématiquement),
- long int .

Chaque taille impose naturellement ses limites.
Toutefois, ces dernières dépendent, non seulement du mot clé considéré,
mais également de la machine utilisée: tous les int n'ont pas la même
taille sur toutes les machines !
Fréquemment, deux des trois mots clés correspondent à une même taille
(par exemple, sur PC, short et int
correspondent à 16 bits, tandis que long correspond à 32 bits).
A titre indicatif, avec 16 bits, on représente des entiers s'étendant
de -32 768 à 32 767; avec 32 bits, on peut couvrir les valeurs allant
de -2 147 483 648 à 2 147 483 647.
Remarque:
en toute rigueur, chacun des trois types (short, int et long) peut être
nuancé par l'utilisation du "qualificatif" unsigned (non signé).
Dans ce cas, il n'y a plus de bit réservé au signe et on ne représente
plus que des nombres positifs.
Son emploi est réservé à des situations particulières.

2.2.3 Notation des constantes entières
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La façon la plus naturelle d'introduire une constante entière dans un
programme est de l'écrire simplement sous forme décimale, avec ou
sans signe, comme dans ces exemples: -533 ou 48...

 2.3 LES TYPES FLOTTANTS
 ~~~~~~~~~~~~~~~~~~~~~~~~

2.3.1 Les différents types et leur représentation en mémoire
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les types "flottants" permettent de représenter, de manière approchée,
une partie des nombres réels. Pour ce faire, ils s'inspirent de la
notation "scientifique" (ou "exponentielle") bien connue qui consiste
à écrire un nombre sous la forme 1.5.10 22 ou 0.472 .10 -8 ;
dans une telle notation, on nomme "mantisses" les quantités telles
que 1.5 ou 0.472 et "exposants" les quantités telles que 22 ou -8.
Le C prévoit trois types de flottants correspondant à des tailles
différentes: float , double (c’est le systématiquement) et long double.
La connaissance des caractéristiques exactes du système de codage n'est
généralement pas indispensable. En revanche, il est important de noter
que de telles représentations sont caractérisées par deux éléments:
- la précision: lors du codage d'un nombre décimal quelconque dans un
type flottant,il est nécessaire de ne conserver qu'un nombre fini de bits.
Or la plupart des nombres s'exprimant avec un nombre limité de décimales
ne peuvent pas s'exprimer de façon exacte dans un tel codage.
On est donc obligé de se limiter à une représentation approchée en
faisant ce qu'on nomme une "erreur de troncature".
Quelle que soit la machine utilisée, on est assuré que cette erreur
(relative) ne dépassera pas 10 -6 pour le type float
et 10 -10 pour le type long double.
- le domaine couvert, c'est-à-dire l'ensemble des nombres représentables à
l'erreur de troncature près. Là encore, quelle que soit la machine
utilisée, on est assuré qu'il s'étendra au moins de 10 -37 à 10 +37 .

 2.3.2 Notation des constantes flottantes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comme dans la plupart des langages, les constantes flottantes peuvent
s'écrire indifféremment suivant l'une des deux notations: décimale et
exponentielle. La notation décimale doit comporter obligatoirement un
point (correspondant à notre virgule). La partie entière ou la partie
décimale peut être omise (mais, bien sûr, pas toutes les deux en même
temps !). En voici quelques exemples corrects:
12.43 -0.38 -.38 4. .27
Par contre, la constante 47 serait considérée comme entière et non comme
flottante. Dans la pratique, ce fait aura peu d'importance, compte tenu
des conversions automatiques qui seront mises en place par le compilateur
(et dont nous parlerons dans le chapitre suivant).

La notation exponentielle utilise la lettre e (ou E) pour introduire un
exposant entier (puissance de 10), avec ou sans signe.
La mantisse peut être n'importe quel nombre décimal ou entier
(le point peut être absent dès que l'on utilise un exposant).
Voici quelques exemples corrects
(les exemples d'une même ligne étant équivalents):
4. 25E4 4 . 25e+4 42. 5E3
54.27E-32 542.7E-33 5427e-34
Par défaut, toutes les constantes sont créées par le compilateur dans le
type double.

 2.4 LES TYPES CARACTÈRES
 ~~~~~~~~~~~~~~~~~~~~~~~~~

2.4.1 La notion de caractère en langage C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le C permet, comme le Pascal, de manipuler des caractères codés en mémoire
sur un octet. Bien entendu, le code employé, ainsi que l'ensemble des
caractères représentables, dépend de l'environnement de programmation
utilisé (c'est-à-dire à la fois de la machine concernée et du compilateur
employé). Néanmoins, on est toujours certain de disposer des lettres
(majuscules et minuscules), des chiffres, des signes de ponctuation et
des différents séparateurs (en fait, tous ceux que l'on emploie pour
écrire un programme !). En revanche, les caractères "nationaux"
(caractères accentués ou ç) ou les caractères "semi-graphiques" ne se
rencontrent pas dans tous les environnements.
Par ailleurs, la notion de caractère en C dépasse celle de caractère
imprimable, c'est-à-dire auquel est obligatoirement associé un graphisme
(et qu'on peut donc imprimer ou afficher sur un écran). C'est ainsi qu'il
existe certains "caractères" de changement de ligne, de tabulation,
d'activation d'une alarme sonore (cloche),...
Nous avons d'ailleurs déjà utilisé le premier (sous la forme \n).

2.4.2 Notation des constantes caractères
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les constantes de type "caractère", lorsqu'elles correspondent à des
caractères imprimables, se notent de façon classique, en écrivant entre
apostrophes (ou quotes) le caractère voulu, comme dans ces
exemples :'Y', ‘+', '$'
Certains caractères non imprimables possèdent une représentation
conventionnelle utilisant le caractère " \caractères (\, ', " et ?) qui,
bien que disposant d'un graphisme,jouent un rôle particulier de délimiteur
qui les empêche d'être notés de manière classique entre deux apostrophes.
Voici la liste de ces caractères.

NOTATION CODE ASCII ABREVIATION
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
EN C (hexadécimal) USUELLE
\a 07 BEL cloche ou bip (alert ou audible bell)
\b 08 BS Retour arrière (Backspace)
\f 0C FF Saut de page (Form Feed)
\n 0A LF Saut de Ligne (Line Feed)
\r 0D CR Retour chariot (Carriage Return)
\t 09 HT Tabulation horizontaLe (HorizontaL Tab)
\v 0B VT Tabutation verticale (VerticaL Tab)
\\ SC \
\’ 2C ‘
\" 22 "
\? 3F ?

2.5 INITIALISATION ET CONSTANTES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La directive
#define n 100 permet de définir une constante s’appelant n et valant
dans tout le programme 100.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\03. LES OPÉRATEURS ET LES EXPRESSIONS EN LANGAGE C.rtf

3. LES OPÉRATEURS ET LES EXPRESSIONS EN LANGAGE C

3.1 L'ORIGINALITÉ DES NOTIONS D'OPÉRATEUR ET D'EXPRESSION EN LANGAGE C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le langage C est certainement l'un des langages les plus fournis en opéra-
teurs. Cette richesse se manifeste tout d'abord au niveau des opérateurs
classiques (arithmétiques, relationnels, logiques) ou moins classiques
(manipulations de bits). Mais, de surcroît, le C dispose d'un important
éventail d'opérateurs originaux d'affectation et d'incrémentation.

3.2 LES OPÉRATEURS ARITHMÉTIQUES EN C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.2.1 Présentation des opérateurs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comme tous les langages, C dispose d'opérateurs classiques "binaires"
(c'est-à-dire portant sur deux "opérandes"), à savoir l'addition (+),
la soustraction (-), la multiplication (*) et la division (/),ainsi que
d'un opérateur "unaire" (c'est-à-dire ne portant que sur un seul opérande)
correspondant à l'opposé noté - (comme dans -n ou -x+y).
Les opérateurs binaires ne sont a priori définis que pour deux opérandes
ayant le même type parmi: int, long int, float, double, long double et
ils fournissent un résultat de même type que leurs opérandes.
De plus, il existe un opérateur de "modulo" noté % qui ne peut porter
que sur des entiers et qui fournit le reste de la division de son premier
opérande par son second. Par exemple, 11%4 vaut 3, 23%6 vaut 5...
Notez bien qu'en C le quotient de deux entiers fournit un entier.
Ainsi 5/2 vaut 2; en revanche, le quotient de deux flottants
(noté, lui aussi, /) est bien un flottant
(5.0/2.0 vaut bien approximativement 2.5).
Remarque: Il n'existe pas d'opérateur d'élévation à la puissance.

3.2.2 Les priorités relatives des opérateurs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lorsque plusieurs opérateurs apparaissent dans une même expression, il est
nécessaire de savoir dans quel ordre ils sont mis en jeu.
En C, comme dans les autres langages, les règles sont "naturelles" et
rejoignent celles de l'algèbre traditionnelle

3.2.3 Le cas du type char
~~~~~~~~~~~~~~~~~~~~~~~~~
Une valeur de type caractère peut être considérée de deux façons:
- comme le caractère concerné: a, Z, fin de ligne....
- comme le code de ce caractère, c'est-à-dire un motif de 8 bits;
or à ce dernier on peut toujours faire correspondre un nombre entier
(le nombre qui, codé en binaire, fournit le motif en question);
par exemple, dans le code ASCII, le caractère A est représenté par le
motif binaire 01000001, auquel on peut faire correspondre le nombre 65.

3.3 LES OPÉRATEURS RELATIONNELS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comme tout langage, C permet de "comparer" des expressions à l'aide
d'opérateurs classiques de comparaison.
En voici un exemple : 2*a > b +5
Par contre, C se distingue de la plupart des autres langages sur deux
points:
- le résultat de la comparaison est, non pas une valeur "booléenne"
(on dit aussi "logique") prenant l'une des deux valeurs vrai ou faux,
mais un entier valant:

Le langage C 8

0 si le résultat de la comparaison est faux,
1 si le résultat de la comparaison est vrai.
Ainsi,la comparaison ci-dessus devient en fait une expression de type entier.
Cela signifie qu'elle pourra éventuellement intervenir dans des calculs
arithmétiques;
Remarque importante: comparaisons de caractères
Compte tenu des règles de conversion, une comparaison peut porter sur
deux caractères. Bien entendu, la comparaison d'égalité ne pose pas de
problème particulier; par exemple (cl et c2 étant de type char):
cl == c2 sera vraie si cl et c2 ont la même valeur,
c'est-à-dire si cl et c2 contiennent des caractères de même code,
donc si cl et c2 contiennent le même caractère, cl == 'e' sera vraie si
le code de cl est égal au code de 'e',donc si cl contient le caractère e.
L'ordre alphabétique est respecté pour les minuscules d'une part, pour
les majuscules d'autre part; on a toujours 'a' < 'c', 'C' < 'S'...
- les chiffres sont dans leur ordre naturel; on a toujours '2' < '5'...

3.4 LES OPÉRATEURS LOGIQUES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C dispose de trois opérateurs logiques classiques:
et (noté &&), ou (noté | |) et non (noté ! ).
Par exemple:
(a<b) && (c<d) prend la valeur 1 (vrai) si les 2 expressions a<b et c<d
sont toutes deux vraies ( de valeur 1), la valeur 0 (faux) dans le cas
contraire.
Il est important de constater que, ne disposant pas de type logique,
C se contente de représenter vrai par 1 et faux par 0.
C'est pourquoi ces opérateurs produisent un résultat numérique
(de type int).
De plus, on pourrait s'attendre à ce que les opérandes de ces opérateurs
ne puissent être que des expressions prenant soit la valeur 0,
soit la valeur 1. En fait, ces opérateurs acceptent n'importe quel
opérande numérique, y compris les types flottants, avec les règles de
conversion implicite déjà rencontrées. Leur signification reste celle
évoquée ci-dessus, à condition de considérer que:
- 0 correspond à faux,
- toute valeur non nulle(et donc pas seulmnt la valeur 1) correspond à vrai.

3.5 L'OPÉRATEUR D'AFFECTATION ORDINAIRE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons déjà eu l'occasion de remarquer que : i = 5 était une expression
qui réalisait une action : l'affectation de la valeur 5 à i.
Cet opérateur d'affectation (=) peut faire intervenir d'autres
expressions comme dans : c=b + 3
La faible priorité de cet opérateur = (elle est inférieure à celle de
tous les opérateurs arithmétiques et de comparaison) fait qu'il y a
d'abord évaluation de l'expression b + 3.
La valeur ainsi obtenue est ensuite affectée à c.
En revanche, il n'est pas possible de faire apparaître une expression
comme premier opérande de cet opérateur =.
Ainsi, l'expression suivante n'aurait pas de sens:c +5 =x

3.6 LES OPÉRATEURS D'INCRÉMENTATION ET DE DÉCRÉMENTATION
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.6.1 Leur rôle
~~~~~~~~~~~~~~~

Le langage C 9

Dans des programmes écrits dans un langage autre que C,on rencontre souvent
des expressions (ou des instructions) telles que : i = i + 1, n = n - 1
qui "incrémentent" ou qui "décrémentent" de 1 la valeur d'une "variable" .
En C, ces actions peuvent être réalisées par des opérateurs "unaires"
portant sur cette "lvalue". Ainsi, l'expression: ++i a pour effet
d'incrémenter de 1 la valeur de i, et sa valeur est celle de i après
incrémentation.
Là encore, comme pour l'affectation,nous avons affaire à une expression
qui non seulement possède une valeur, mais qui, de surcroît, réalise une
action (incrémentation de i).
Il est important de voir que la valeur de cette expression est celle de
i après incrémentation. Ainsi, si la valeur de i est 5, l'expression :
n = ++i-5 affectera à i la valeur 6 et à n la valeur 1.
En revanche, lorsque cet opérateur est placé après la "lvalue" sur
laquelle il porte, la valeur de l'expression correspondante est celle de
la variable avant incrémentation. Ainsi, si i vaut 5,
l'expression: n = i++ - 5 affectera à i la valeur 6 et à n la valeur 0
(car ici la valeur de l'expression i++ est 5).
On dit que ++ est:
- un opérateur de pré incrémentation lorsqu'il est placé à gauche
de la "lvalue" sur laquelle il porte,
- un opérateur de post incrémentation lorsqu'il est placé à droite
de la "lvalue" sur laquelle il porte.
Bien entendu, lorsque seul importe l'effet d'incrémentation d'une "lvalue",
cet opérateur peut être indifféremment placé avant ou après.
Ainsi, ces deux instructions (ici, il s'agit bien d'instructions car les
expressions sont terminées par un point-virgule-leur valeur se trouve donc
inutilisée) sont équivalentes:i++; ++i;

3.6.2 Leurs priorités
~~~~~~~~~~~~~~~~~~~~~~
Les priorités élevées de ces opérateurs unaires
(voir tableau en fin de chapitre) permettent d'écrire des expressions
assez compliquées sans qu'il soit nécessaire d'employer des parenthèses
pour isoler la "Ivalue" sur laquelle ils portent.
Ainsi, l'expression suivante a un sens:
3 * i++ *j-- + k++
(si * avait été plus prioritaire que la post incrémentation, ce dernier
aurait été appliqué à l'expression 3*i qui n'est pas une "lvalue";
l'expression n'aurait alors pas eu de sens).
Remarque:
Il est toujours possible (mais non obligatoire) de placer un ou
plusieurs espaces entre un opérateur et les opérandes sur lesquels il
porte. Nous utilisons souvent cette latitude pour accroître la lisibilité
de nos instructions.
Cependant, dans le cas des opérateurs d'incrémentation, nous avons
plutôt tendance à ne pas le faire, cela pour mieux "rapprocher"
l'opérateur de la "lvalue" sur laquelle il porte.

3.6.3 Leur intérêt
~~~~~~~~~~~~~~~~~~
Ces opérateurs allègent l'écriture de certaines expressions et offrent
surtout le grand avantage d'éviter la "redondance" qui est de mise dans
la plupart des autres langages. En effet, dans une notation telle que:
i++ on ne "cite" qu'une seule fois la "lvalue" concernée alors qu'on est
amené à le faire deux fois dans la notation:i=i+ 1.
Les risques d'erreurs de programmation s'en trouvent ainsi quelque peu
limités. Bien entendu, cet aspect prendra d'autant plus d'importance que
la "lvalue" correspondante sera d'autant plus complexe.
D'une manière générale, nous utiliserons fréquemment ces opérateurs dans
la manipulation de tableaux ou de chaînes de caractères.Ainsi,anticipant
sur les chapitres suivants, nous pouvons indiquer qu'il sera possible de
lire l'ensemble des valeurs d'un tableau nommé t en répétant la seule
instruction:
t [i++] = getchar();
Celle-ci réalisera à la fois:
- la lecture d'un caractère au clavier,

Le langage C 10

- l'affectation de ce caractère à l'élément de rang i du tableau t,
- I'incrémentation de 1 de la valeur de i (qui sera ainsi préparée pour
la lecture du prochain élément).

3.7 LES OPÉRATEURS D'AFFECTATION ÉLARGIE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous venons de voir comment les opérateurs d'incrémentation permettaient
de simplifier l'écriture de certaines affectations.
Par exemple:i++ remplaçait avantageusement: i=i+ 1
Mais C dispose d'opérateurs encore plus puissants.
Ainsi, vous pourrez remplacer: i = i + k par : i += k ou, mieux
encore : a = a*b par : a *= b.
D'une manière générale, C permet de condenser les affectations de la
forme : lvalue = lvalue opérateur expression en :
lvalue opérateur= expression
Cette possibilité concerne tous les opérateurs binaires arithmétiques
et de manipulation de bits.
Voici la liste complète de tous ces nouveaux opérateurs nommés
"opérateurs d'affectation élargie": += -= *= /= %= |= ...
Ces opérateurs, comme ceux d'incrémentation, permettent de condenser
l'écriture de certaines instructions et contribuent à éviter la redondance
introduite fréquemment par l'opérateur d'affectation classique.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\04. LES ENTRÉES-SORTIES CONVERSATIONELLES.rtf

4. LES ENTRÉES-SORTIES CONVERSATIONELLES

4.1 LES POSSIBILITÉS DE LA FONCTION PRINTF
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons déjà vu que le premier argument de printf est une chaîne de
caractères qui spécifie à la fois :
- des caractères à afficher tels quels,
- des "codes de format'' repérés par %.
Un "code de conversion" (tel que c, d ou f) y précise le type de
l'information à afficher.
D'une manière générale, il existe d'autres caractères de conversion soit
pour d'autres types de valeurs, soit pour agir sur la précision de
l'information que l'on affiche. De plus, un code de format peut contenir
des informations complémentaires agissant sur le ''cadrage'',le ''gabarit''
ou la ''précision''.
Ici, nous nous limiterons aux possibilités les plus usitées de printf.

4.1.1 Les principaux codes de conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
c char: caractère affiché ''en clair"
(convient aussi à short ou à int compte tenu des conversions systématiques)

d int (convient aussi à char ou à int,compte tenu des conversions systém.)
u unsigned int (convient aussi à unsigned char ou à unsigned short,
compte tenu des conversions systématiques)
ld long
lu unsigned long
f double ou float (compte tenu des conversions systématiques
float - > double) écrit ennotation "décimale" avec six chiffres après
le point (par exemple: 1.234500 ou 123.456789)
e double ou float (compte tenu des conversions systématiques
float - > double) écrit en notation ‘’exponentielle’’ (mantisse entre
1 inclus et 10 exclu) avec six chiffres après le point décimal,
sous la forme x.xxxxxxe+yyy ou x.xxxxxxe-yyy pour les nombres positifs
et -x.xxxxxxe+yyy ou -x.xxxxxxe-yyy pour lesnombres négatifs
s chaîne de caractères dont on fournit l'adresse
(notion qui sera étudiée ultérieurement)

4.1.2 Action sur le gabarit d'affichage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Par défaut, les entiers sont affichés avec le nombre de caractères néces-
saires (sans espaces avant ou après).
Les flottants sont affichés avec six chiffres après le point
(aussi bien pour le code e que f).
Un nombre placé après % dans le code de format précise un gabarit
d'affichage, c'est-à-dire un nombre minimal de caractères à utiliser.
Si le nombre peut s'écrire avec moins de caractères, printf le fera
précéder d'un nombre suffisant d'espaces; en revanche, si le nombre
ne peut s'afficher convenablement dans le gabarit imparti, printf
utilisera le nombre de caractères nécessaires.

4.1.3 La macro putchar
~~~~~~~~~~~~~~~~~~~~~~
L' expression putchar() joue le même rôle que: printf ('' %c'' , c)
Son exécution est toutefois plus rapide, dans la mesure où elle ne fait
pas appel au mécanisme d'analyse de format.
Notez qu'en toute rigueur putchar n'est pas une vraie fonction mais
une ''macro". Ses instructions (écrites en C) seront incorporées à votre
programme par la directive:

Le langage C 12

#include <stdio.h>
Alors que cette directive était facultative pour printf
(qui est une fonction), elle devient absolument nécessaire pour putchar.
En son absence, l'éditeur de liens serait amené à rechercher une fonction
putchar en bibliothèque et, ne la trouvant pas, il vous gratifierait d'un
message d'erreur.

4.2 LES POSSIBILITÉS DE LA FONCTION SCANF
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons déjà rencontré quelques exemples d'appels de scanf.
Nous y avons notamment vu la nécessité de recourir à l'opérateur & pour
désigner l'adresse de la variable (plus généralement de la "lvalue")
pour laquelle on souhaite lire une valeur.
Vous avez pu remarquer que cette fonction possédait une certaine
ressemblance avec printf et qu'en particulier elle faisait, elle aussi,
appel à des ''codes de format''.
Cependant, ces ressemblances masquent également des différences assez
importantes au niveau :
- de la signification des codes de format.Certains codes correspondront à des
types différents, suivant qu'ils sont employés avec printf ou avec scanf;
- de l'interprétation des caractères du format qui ne font pas partie d'un
code de format.
En revanche, compte tenu de la complexité de scanf,nous vous en exposerons
les différentes possibilités de façon progressive, à l'aide d'exemples.
Notamment, ce n'est qu'à la fin de ce chapitre que vous serez en mesure de
connaître toutes les conséquences de données incorrectes.

4.2.1 Les principaux codes de conversion de scanf
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pour chaque code de conversion, nous précisons le type de la ''lvalue''
c char
d int
u unsigned int
hd short int
hu unsigned short
ld long int
lu unsigned long
f ou e float écrit indifféremment dans l'une des deux notations:
décimale (éventuellement sans point, c'est-à-dire comme un entier) ou
exponentielle (avec la lettre e ou E)
lf ou le double avec la même présentation que ci-dessus
s chaîne de caractères dont on fournit l' adresse
(notion qui sera étudiée ultérieurement)
Remarque:
Contrairement à ce qui se passait pour printf, il ne peut plus y avoir
ici de conversion automatique puisque l'argument transmis à scanf est
l'adresse d'un emplacement mémoire.
C'est ce qui justifie l'existence d'un code hd par exemple pour le type
short ou encore celle des codes lf et le pour le type double.

4.2.2 La macro getchar
~~~~~~~~~~~~~~~~~~~~~~

Le langage C 13

L'expression : c = getchar() joue le même rôle que : scanf ( '' %c'' , &c)
tout en étant plus rapide puisque ne faisant pas appel au mécanisme
d'analyse d'un format.
Notez bien que getchar utilise le même tampon(image d'une ligne)que scanf.
En toute rigueur, getchar est une ''macro'' (comme putchar) dont les
instructions figurent dans stdio.h.
Là encore, l'omission d'une instruction #include appropriée conduit à une
erreur à l'édition de liens.

4.3 Exercice :
~~~~~~~~~~~~~~
Utiliser les instructions printf et scanf pour afficher des nombres
quelconques entiers ou réels entrés par l’utilisateur.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\05. LES INSTRUCTIONS DE CONTROLES.rtf

5. LES INSTRUCTIONS DE CONTROLES

5.1 L'INSTRUCTION IF
~~~~~~~~~~~~~~~~~~~~
Nous avons déjà rencontré des exemples d'instruction if et nous avons vu
que cette dernière pouvait éventuellement faire intervenir un ''bloc''.
Précisons donc tout d'abord ce qu'est un bloc d'une manière générale.

5.1.1 Blocs d'instructions
~~~~~~~~~~~~~~~~~~~~~~~~~~
Un bloc est une suite d'instructions placées entre { et }.
Les instructions figurant dans un bloc sont absolument quelconques.
Il peut s'agir aussi bien d'instructions simples (terminées par un
point-virgule)que d'instructions structurées (choix, boucles) lesquelles
peuvent alors à leur tour renfermer d'autres blocs...
Rappelons qu'il y a en C , comme en Pascal , une sorte de récursivité de
la notion d'instruction.Dans la description de la syntaxe des différentes
instructions, nous serons souvent amené à mentionner ce terme
d'instruction. Comme nous l'avons déjà noté,celui-ci désignera toujours
n'importe quelle instruction C : simple, structurée ou un bloc.
Un bloc peut se réduire à une seule instruction, voire être ''vide''.
Voici deux exemples de blocs corrects :

{} {i= 1 ;}

Le second bloc ne présente aucun intérêt en pratique puisqu'il pourra
toujours être remplacé par l'instruction simple qu'il contient.
En revanche, nous verrons que le premier bloc (lequel pourrait a priori
être remplacé par... rien) apportera une meilleure lisibilité dans le cas
de boucles ayant un ''corps'' vide.
Notez encore que {; } est un bloc constitué d'une seule instruction
''vide'', ce qui est ''syntaxiquement'' correct.
Remarque importante: N'oubliez pas que toute instruction simple est
toujours terminée par un point-virgule.
Ainsi, ce bloc :{i =5;k=3}est incorrect car il manque un point-virgule
à la fin de la seconde instruction qu'il contient.
D'autre part, un bloc joue le même rôle syntaxique qu'une instruction
simple (point virgule compris). Evitez donc d'ajouter des points-virgules
intempestifs à la suite d'un bloc.

5.1.2 Syntaxe de l'instruction if
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le mot else et l'instruction qu'il introduit sont facultatifs.
Remarque:
Notez bien que la syntaxe de cette instruction n'impose en soi aucun
point-virgule, si ce n'est ceux qui terminent naturellement les
instructions simples qui y figurent.
Un else se rapporte toujours au dernier if rencontré auquel un else
n'a pas encore été attribué.

5.2 L'instruction switch
~~~~~~~~~~~~~~~~~~~~~~~~
switch (expression)
( case constante_1 : [ suite_d'instructions_1 ]
case constante_2 : [ suite_d'instructions_2 ]
..............
case constante_n : [ suite_d'instructions_n ]

Le langage C 15

[ default : suite_d'instructions ]

expression : expression entière quelconque,
constante : expression constante d'un type entier quelconque
(char est accepté car il sera converti en int),
suite_d'instructions: séquence d'instructions quelconques.
N.B.:
les crochets ( [ et ] ) signifient que ce qu'ils renferment est facultatif.

5.3 L’instruction do... while
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
do instruction
while (expression);
1) Notez bien,la présence de parenthèses autour de l'expression qui régit
la poursuite de la boucle,
et la présence d'un point-virgule à la fin de cette instruction.

5.4 L’instruction while
~~~~~~~~~~~~~~~~~~~~~~~~
while (expression)
instruction
Commentaires
1) Là encore, notez bien la présence de parenthèses pour délimiter la
condition de poursuite. Remarquez que, par contre, la syntaxe n'impose
aucun point-virgule de fin (il s'en trouvera naturellement un à la fin
de l'instruction qui suit si celle-ci est simple).
2) L'expression utilisée comme condition de poursuite est évaluée avant le
premier tour de boucle.
Il est donc nécessaire que sa valeur soit définie à ce moment.
3) Lorsque la condition de poursuite est une expression qui fait appel à
l'opérateur séquentiel, n'oubliez pas qu'alors toutes les expressions qui
la constituent seront évaluées avant le''test de poursuite'' de la boucle.

5.5 L'INSTRUCTION FOR
~~~~~~~~~~~~~~~~~~~~~~
Etudions maintenant la dernière instruction permettant de réaliser des
boucles, à savoir I' instruction for.

5.5.1 Exemple d'introduction de l'instruction for
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Considérez ce programme:
#include <stdio.h>
int main(void)
{ int i ;
for (i=1 ; i<=5 ; i++ )
{ printf ("bonjour ") ;
printf ("°/.d fois\n", i);
}
return 0 ;
}

Le langage C 16

for ( i=1 ; i<=5 ; i++ ) comporte en fait trois expressions.
La première est évaluée (une seule fois) avant d'entrer dans la boucle.
La deuxième conditionne la poursuite de la boucle.
Elle est évaluée avant chaque parcours.
La troisième, enfin, est évaluée à la fin de chaque parcours.

5.5.2 Syntaxe de l’instruction FOR
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
for(expression_1;expression_2;expression_3)
instruction

5.6 Exercices :
~~~~~~~~~~~~~~~~
1) Soit le programme suivant :
#include <stdio.h>
int main(void)
{ int i,n,som;
som = 0;
for(i=0;i<4;i++)
{ printf(‘’donnez un entier ’’);
scanf(‘’ %d ’’,&n);
som+=n;
}
printf(‘’somme : %d\n’’,som);
return 0 ;
}
Ecrire un programme réalisant exactement la même chose, en employant :
a) une instruction while;
b) une instruction do ... while.
2) Calculer la moyenne de notes fournies au clavier. Le nombre de notes
n’est pas connu à priori et l’utilisateur peut en fournir autant qu’il
le désire. Pour signaler qu’il a terminé on convient de fournir une
note négative non comptée dans la moyenne.
3) Afficher un triangle rempli d’étoiles, s’étendant sur un nombre de
lignes fourni en donnée et se présentant ainsi :
*
**
***
****
4) Déterminer si un nombre entier fourni en donnée est premier ou non.
5) Ecrire un programme qui détermine la nième valeur de la suite de
Fibonacci définie comme suit :
u1 = 1 ; u2 = 1 ; un = un-1 + u n-2 pour n > 2.
6) Ecrire un programme qui affiche la table de multiplication des
nombres de 1 à 10, sous la forme suivante :
I 1 2 3 4 5 6 7 8 9 10
--------------------------------------------------------------------------------------------------------------
1 I 1 2 3 4 5 6 7 8 9 10
2 I 2 4 6 8 10 12 14 16 18 20
...
10 I 10 20 30 40 50 60 70 80 90 100


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\06. LA PROGRAMMATION MODULAIRE ET LES FONCTIONS.rtf

6. LA PROGRAMMATION MODULAIRE ET LES FONCTIONS
Comme tous les langages, C permet de découper un programme en plusieurs
parties nommées souvent "modules".
Cette programmation dite"modulaire"se justifie pour de multiples raisons:
- Un programme écrit d'un seul tenant devient difficile à comprendre dès
qu'il dépasse une ou deux pages de texte.
Une écriture modulaire permet de le scinder en plusieurs parties et de
regrouper dans le "programme principal" les instructions en décrivant les
enchaînements. Chacune de ces parties peut d'ailleurs, si nécessaire,
être décomposée à son tour en modules plus élémentaires; ce processus
de décomposition pouvant être répété autant de fois que nécessaire,
comme le préconisent les méthodes de "programmation structurée".
- La programmation modulaire permet d'éviter des séquences d'instructions
répétitives, et cela d'autant plus que la notion d'argument" permet
de "paramétrer" certains modules.
- La programmation modulaire permet le partage d'outils communs qu'il
suffit d'avoir écrits et mis au point une seule fois.
Cet aspect sera d'autant plus marqué que C autorise effectivement
la compilation séparée de tels modules.

6.1 LA FONCTION: LA SEULE SORTE DE MODULE EXISTANT EN C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
En C, il n'existe qu'une seule sorte de module, nommé fonction.
Ce terme, quelque peu abusif, pourrait laisser croire que les modules
du C sont moins généraux que ceux des autres langages.
Or il n'en est rien, bien au contraire ! Certes, la fonction pourra y
être utilisée comme dans d'autres langages, c'est-à-dire recevoir des
arguments et fournir un résultat scalaire qu'on utilisera dans une
expression, comme, par exemple, dans :
y = sqrt(x)+3;

Mais, en C, la fonction pourra prendre des aspects différents, pouvant
complètement dénaturer l'idée qu'on se fait d'une fonction.
Par exemple:
- La valeur d'une fonction pourra très bien ne pas être utilisée;
c'est ce qui se passe fréquemment lorsque vous utilisez printf ou scanf.
Bien entendu, cela n'a d'intérêt que parce que de telles fonctions
réalisent une action (ce qui, dans d'autres langages, serait réservé aux
sous-programmes ou procédures).
Une fonction pourra ne fournir aucune valeur.
- Une fonction pourra fournir un résultat non scalaire (nous n'en parlerons
toutefois que dans le chapitre consacré aux structures.
- Une fonction pourra modifier les valeurs de certains de ses arguments
(il vous faudra toutefois attendre d' avoir étudié les pointeurs pour
voir par quel mécanisme elle y parviendra).
Ainsi, donc, malgré son nom, en C,la fonction pourra jouer un rôle aussi
général que la procédure ou le sous-programme des autres langages.

6.2 EXEMPLE DE DÉFINITION ET D'UTILISATION D'UNE FONCTION EN C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous vous proposons d'examiner tout d'abord un exemple simple de fonction
correspondant à l'idée usuelle que l'on se fait d'une fonction,
c'est-à-dire recevant des arguments et fournissant une valeur.

/***** le programme principal (fonction main) *****/
int main(void)
{
double fexple (double m, int n, int p) ; /* déclaration de la fonction fexple */
double x = 1.5 ;
double y, z ;
int n = 3, p = 5, q = 10 ;
/* appel de fexple avec les arguments x, n et p */
y = fexple (x, n, p) ;
printf ("valeur de y : %f\n", y) ;

Le langage C 18

/* appel de fexple avec les arguments x+0.5, q et n-1 */
z = fexple (x+0.5, q, n-1) ;
printf ("valeur de z : %f\n", z) ;
return 0 ;
}
/*************** la fonction fexple ****************/
double fexple (fdouble x, int b, int c)
{
double val ; /* déclaration d'une variable "locale" à fexple*/
val = x * x + b * x + c ;
return val ;
}

Nous y trouvons tout d'abord, de façon désormais classique, un prgrm
principal formé l'un bloc. Mais, cette fois, à sa suite, apparaît la
définition d'une fonction.
Celle-ci possède me structure voisine de la "fonction" main,
à savoir un en-tête et un corps délimité par des accolades ({ et }).
Mais l'en-tête est plus élaboré que celui de la fonction main puisque,
outre le nom de la fonction (fexple), on y trouve une "liste d'arguments"
(nom + type), ainsi que le type de la valeur qui sera fournie par la
fonction (on la nomme indifféremment résultat ‘’valeur de la fonction",
"valeur de retour"...):
double fexple (double x, int b, int c) type de la nom de la premier
deuxième troisième "valeur fonction argument argument argument de retour"
(type double) (type int) (type int)
Les noms des arguments n'ont d'importance qu'au sein du corps de la
fonction.Ils servent à décrire le travail que devra effectuer la fonction
quand on l'appellera en lui fournissant trois valeurs.
Si on s'intéresse au corps de la fonction, on y rencontre tout d'abord
une déclaration: double val ;
Celle-ci précise que, pour effectuer son travail, notre fonction a
besoin d'une variable de type double nommée val.
On dit que val est une "variable locale" à la fonction fexple,
de même que les variables telles que n, p, y...
sont des variables locales à la fonction main
(mais comme jusqu'ici nous avions affaire à un programme constitué d'une
seule fonction, cette distinction n'était pas utile).
Un peu plus loin,nous examinerons plus en détail cette notion de variable
locale et celle de "portée" qui s'y attache.
L'instruction suivante de notre fonction fexple est une affectation clas-
sique (faisant toutefois intervenir les valeurs des arguments x, n et p).
Enfin, l' instruction return val précise la valeur que fournira la fonction
à la fin de son travail.
En définitive, on peut dire que fexple est une fonction telle que
fexple (x, b, c) fournisse la valeur de l'expression x 2 + bx + c.

Examinons maintenant la fonction main.
Vous constatez qu'on y trouve une déclaration:
double fexple (double m, int n , int p) ;
Elle sert à prévenir le compilo que fexple est une fonction et elle lui
précise le type de ses arguments ainsique celui de sa valeur de retour.
Nous reviendrons plus loin en détail sur le rôle d'une telle déclaration.
L'utilisation de notre fonction fexple au sein de la fonction main,
elle est classique et comparable à celle d'une fonction prédéfinie telle
que scanf ou sqrt.
Ici, nous nous sommes contenté d'appeler notre fonction à deux reprises
avec des arguments différents.

Le langage C 19

6.3 QUELQUES RÈGLES
~~~~~~~~~~~~~~~~~~~~
6.3.1 Arguments muets et arguments effectifs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les noms des arguments figurant dans l'en-tête de la fonction se nomment des
"arguments muets" (ou encore "arguments formels" ou "paramètres formels").
Leur rôle est de permettre, au sein du corps de la fonction,
de décrire ce qu'elle doit faire.

6.3.2 L'instruction return
~~~~~~~~~~~~~~~~~~~~~~~~~~
Voici quelques règles générales concernant cette instruction.
- L'instruction return peut mentionner n'importe quelle expression.
Ainsi, nous aurions pu définir la fonction fexple précédente d'une
manière plus simple:
double fexple (double x, int b, int c)
{ return (x * x + b * x + c) ;
}
- L'instruction return peut apparaître à plusieurs reprises dans une
fonction, comme dans cet autre exemple: double absom (double u, double v)
{
double s ;
s = a + b ;
if (s>0) return (s) ;
else return (-s)
}
Notez bien que non seulement l'instruction return définit la valeur du
résultat, mais, en même temps, elle interrompt l'exécution de la
fonction en revenant dans la fonction qui l'a appelée.
Nous verrons qu'une fonction peut ne fournir aucune valeur: aucune
instruction return ne figurera alors dans sa définition.
Dans ce cas (absence d'instruction return), le retour est mis en place
automatiquement par le compilateur à la "fin" de la fonction.
- Si le type de l'expression figurant dans return est différent du type du
résultat tel qu'il a été déclaré dans l'en-tête, le compilateur mettra
automatiquement en place des instructions de conversion.
Il est toujours possible de ne pas utiliser le résultat d'une fonction,
même si elle en produit un.
C'est d'ailleurs ce que nous avons fait fréquemment avec printf ou scanf.
Bien entendu, cela n'a d'intérêt que si la fonction fait autre chose que
de calculer un résultat. En revanche,il est interdit d'utiliser la valeur
d'une fonction ne fournissant pas de résultat (si certains compilateurs
l'acceptent, vous obtiendrez,lors de l'exécution, une valeur aléatoire!).

6.3.3 Cas des fonctions sans valeur de retour ou sans arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Quand une fonction ne renvoie pas de résultat, on le précise,à la fois
dans l'en-tête et dans sa déclaration, à l'aide du mot clé void.
Par exemple, voici l'en-tête d'une fonction recevant un argument de type
int et ne fournissant aucune valeur:
void sansval (int n)
et voici quelle serait sa déclaration:
void sansval (int) ;
Naturellement, la définition d'une telle fonction ne doit, en principe,
contenir aucune instruction return.
Quand une fonction ne reçoit aucun argument, on place le mot clé void
(le même que précédemment, mais avec une signification différente !)
à la place de la liste d'arguments.
Voici l'en-tête d'une fonction ne recevant aucun argument et renvoyant
une valeur de type float (il pourrait s'agir,par exemple,d'une fonction
fournissant un nombre aléatoire !):

Le langage C 20

double tirage (void)
Sa déclaration serait très voisine (elle ne diffère que par la présence
du point-virgule !): double tirage (void) ;
Enfin,rien n'empêche de réaliser une fonction ne possédant ni arguments
ni valeur de retour. Dans ce cas, son en-tête sera de la forme:
void message (void)
et sa déclaration sera:
void message (void);

6.4 EN C, LES ARGUMENTS SONT TRANSMIS PAR VALEUR
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons déjà eu l'occasion de dire qu'en C les arguments d'une fonction
étaient transmis ''par valeur''. Cependant, dans les exemples que nous
avons rencontrés dans ce chapitre, les conséquences et les limitations de
ce mode de transmission n'apparaissaient guère. Or voyez cet exemple:
#include <stdio.h>
int main(void)
{ void echange (int a, int b) ;
int n=10, p=20 ;
printf ("avant appel: %d %d\n", n, p) ;
echange (n, p) ;
printf ("après appel: %d %d", n, p) ;
return 0 ;
}
void echange (int a, int b)
{ int c ;
printf ("début echange : %d %d\n", a, b) ;
c = a ;
a = b ;
b = c ;
printf ("fin echange : °/d %d\n", a, b) ;
}
avant appel : 10 20 début echange : 10 20 fin echange :
20 10 après appel : 10 20
La fonction echange reçoit deux valeurs correspondant à ses deux arguments
muets a et b. Elle effectue un échange de ces deux valeurs.
Mais, lorsque l'on est revenu dans le programme principal, aucune trace
de cet échange ne subsiste sur les arguments effectifs n et p.
En effet, lors de l'appel de echange, il y a eu transmission de la
valeur des expressions n et p.
On peut dire que ces valeurs ont été recopiées "localement" dans la
fonction echange dans des emplacements nommés a et b.
C'est effectivement sur ces copies qu'a travaillé la fonction echange,
de sorte que les valeurs des variables n et p n'ont, quant à elles, pas
été modifiées. C'est ce qui explique le résultat constaté.
Ce mode de transmission semble donc interdire a priori qu'une fonction
produise une ou plusieurs valeurs "en retour", autres que celle de la
fonction elle-même.
Or, il ne faut pas oublier qu'en C tous les "modules" doivent être
écrits sous forme de fonction. Autrement dit,ce simple problème d'échange
des valeurs de deux variables doit pouvoir se résoudre à l'aide d'une
fonction.
Nous verrons que ce problème possède plusieurs solutions, à savoir:

Le langage C 21

- Transmettre en argument la "valeur'' de l'adresse" d'une variable.
La fonction pourra éventuellement agir sur le "contenu" de cette adresse.
C'est ce que nous faisons lorsque nous utilisons la fonction scanf.
Nous examinerons cette technique en détail dans le chapitre consacré
aux "pointeurs".
- Utiliser des "variables globales", comme nous le verrons dans le prochain
paragraphe; cette deuxième solution devra toutefois être réservée à des
cas exceptionnels, compte tenu des risques qu'elle présente
("effets de bords").
Remarque:
C'est bien parce que la transmission des arguments se fait "par valeur"
que les arguments effectifs peuvent prendre la forme d'une expression
quelconque. Dans les langages où le seul mode de transmission est
celui "par adresse", les arguments effectifs ne peuvent être que
l'équivalent d'une lvalue.

6.5 LES VARIABLES GLOBALES
~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons vu comment échanger des informations entre différentes fonctions
grâce à la transmission d'arguments et à la récupération d'une valeur de
retour.
En fait, en C comme en Pascal, plusieurs fonctions (dont, bien entendu
le programme principal main) peuvent partager des variables communes
qu'on qualifie alors de globales
Les variables globales ne sont connues du compilateur que dans la partie
du programme source suivant leur déclaration.

6.6 LES VARIABLES LOCALES
~~~~~~~~~~~~~~~~~~~~~~~~~~
Les variables locales ne sont connues qu’à l’intérieur de la fonction où
elles sont déclarées. Il est toutefois possible de demander d’attribuer
un emplacement permanent à une varaible locale et qu’ainsi sa valuer se
conserve d’un appel au suivant.
Il suffit pour cela de la déclarer à l’aide du mot clé STATIC.

6.7 LES FONCTIONS RECURSIVES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Le langage C autorise la récursivité des appels de fonctions.
Exemple : long fac(n)
{ if (n>1) return (fac(n-1)*n);
else return (1);
}

6.8 Exercices
~~~~~~~~~~~~~~
1) Ecrire:
- une fonction, nommée f1, se contentant d'afficher ''bonjour''
(elle ne possédera aucun argument ni valeur de retour),
- une fonction, nommée f2 qui affiche "bonjour" un nombre de fois égal à
la valeur reçue en argument (int) et qui ne renvoie aucune valeur,
- une fonction, nommée f3 qui fait la même chose que f2 mais qui, de plus,
renvoie la valeur (int) 0.
Ecrire un petit programme appelant successivement chacune de ces trois
fonctions, après les avoir convenablement déclarées sous forme
d'un prototype.

2) Qu’affiche le programme suivant :
int n=5;
int main(void)
{
Le langage C 22
void fct(int p);
int n=3;
fct(n);
return 0 ;
}

void fct(int p)
{
printf(‘’ %d %d ’’,n,p);
}

3) Ecrire une fonction qui se contente de comptabiliser le nombre de fois
où elle a été appelée en affichant seulement un message "de temps
en temps", à savoir:
- au premier appel: *** appel 1 fois ***
- au dixième appel: *** appel 10 fois ***
- au centième appel: *** appel 100 fois ***
- et ainsi de suite pour le millième, le dix millième appel...
On supposera que le nombre maximal d'appels ne peut dépasser la capacité
d'un long.
4) Ecrire une fonction premier permettant de déterminer si un nombre entier
donné est premier.
5) Ecrire un programme donnant la liste des nombres premiers de 1 à N.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\07. LES TABLEAUX ET LES POINTEURS.rtf

7. LES TABLEAUX ET LES POINTEURS
Comme tous les langages, C permet d'utiliser des "tableaux".
On nomme ainsi un ensemble d'éléments de même type désignés par un
identificateur unique; chaque élément est repéré par un "indice"
précisant sa position au sein de l'ensemble.
Par ailleurs, comme certains langages tels que Pascal,le langage C dispose
de "pointeurs", c'est-à-dire de variables destinées à contenir des
adresses d'autres "objets" (variables, fonctions. . .).
A priori, ces deux notions de tableaux et de pointeurs peuvent paraître
fort éloignées l'une de l'autre. Toutefois, il se trouve qu'en C un
lien indirect existe entre ces deux notions,à savoir qu'un identificateur
de tableau est une "constante pointeur".
Cela peut se répercuter dans le traitement des tableaux, notamment
lorsque ceux-ci sont transmis en argument de l'appel d'une fonction.
C'est ce qui justifie que ces deux notions soient regroupées dans un
seul chapitre.

7.1 LES TABLEAUX A UN INDICE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7.1.1 Exemple d'utilisation d'un tableau en C
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Supposons que nous souhaitions déterminer,à partir de vingt notes d'élèves
(fournies en données), combien d'entre elles sont supérieures à la
moyenne de la classe.
S'il ne s'agissait que de calculer simplement la moyenne de ces notes,
il nous suffirait d'en calculer la somme, en les cumulant dans une
variable, au fur et à mesure de leur lecture.
Mais, ici, il nous faut à nouveau pouvoir consulter les notes pour
déterminer combien d'entre elles sont supérieures à la moyenne ainsi
obtenue. Il est donc nécessaire de pouvoir "mémoriser" ces vingt notes.
Pour ce faire, il paraît peu raisonnable de prévoir vingt variables
scalaires différentes (méthode qui,de toute manière,serait difficilement
transposable à un nombre important de notes).
Le tableau va nous offrir une solution convenable à ce problème,
comme le montre le programme suivant.

#include <stdio.h>
int main(void)
{ int i, som, nbm ;
double moy ;
int t[20] ;
for (i=0 ; i<20 ; i++)
{ printf ("donnez la note numéro %d : ", i+1) ;
scanf ("%d", &t[i]) ;
}
for(i=0, som=0 ; i<20 ; i++) som += t[i] ;
moy = som / 20 ;
printf ("\n\n moyenne de la classe : %f\n", moy) ;
for (i=0, nbm=0 ; i<20 ; i++ )
if (t[i] > moy) nbm++ ;
printf ("%d élèves ont plus de cette moyenne", nbm) ;
return 0 ;
}
La déclaration:int t[20] réserve l'emplacement pour 20 éléments de type int.
Chaque élément est repéré par sa ''position" dans le tableau,
nommée "indice".
Conventionnellement,en langage C,la première position porte le numéro 0.
Ici, donc, nos indices vont de 0 à 19.
Le premier élément du tableau sera désigné par t[0],le troisième par t[2]
le dernier par t[l9].
Plus généralement, une notation telle que t[i] désigne un élément dont
la position dans le tableau est fournie par la valeur de i.
Elle joue le même rôle qu'une variable scalaire de type int.

Le langage C 24

7.1.2 Quelques règles
~~~~~~~~~~~~~~~~~~~~~~
a) Les éléments de tableau
Un élément de tableau est une lvalue.
Il peut donc apparaître à gauche d'un opérateur d'affectation comme dans:
t[2] = 5
Il peut aussi apparaître comme opérande d'un opérateur d'incrémentation,
comme dans:
t[3]++ --t[i]
En revanche, il n'est pas possible, si tl et t2 sont des tableaux
d'entiers, d'écrire tl = t2; en fait, le langage C n'offre aucune
possibilité d'affectations globales de tableaux, comme c'était le cas,
par exemple, en Pascal.

b) Les indices
Un indice peut prendre la forme de n'importe quelle expression arithmé-
tique de type entier (ou caractère, compte tenu des règles de conversion
systématique).

c) La dimension d'un tableau
La dimension d'un tableau (son nombre d'éléments) ne peut être qu'une
constante ou une expression constante.
Ainsi, cette construction :
#define N 50
....
int t[N] ; double h[2*N- 1 ];
est correcte. Elle ne le serait pas, par contre, si N était une
constante sym bolique définie par const int N = 50 (les
expressions N et 2*N-1 n'étant alors plus calculables par le compilo).

d) Débordement d'indice
Aucun contrôle de "débordement d'indice" n'est mis en place par la
plupart des compilateurs. Pour en comprendre les conséquences, il faut
savoir que, lorsque le compilateur rencontre une lvalue telle que
t[i] , il en détermine l'adresse en ajoutant à l'adresse de début du
tableau t, un ''décalage" proportionnel à la valeur de i (et aussi
proportionnel à la taille de chaque élément du tableau).
De sorte qu'il est très facile (si l'on peut dire !) de désigner et,
partant, de modifier, un emplacement situé avant ou après le tableau.

7.1.3 LES TABLEAUX À PLUSIEURS INDICES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7.1.4 Leur déclaration
~~~~~~~~~~~~~~~~~~~~~~
Comme tous les langages, C autorise les tableaux à plusieurs indices
(on dit aussi à plusieurs dimensions).
Par exemple, la déclaration:
int t[5][3]
réserve un tableau de 15 (5 x 3) éléments.
Un élément quelconque de ce tableau se trouve alors repéré par deux
indices comme dans ces notations:
t[3][2l t[i][j] t[i-3][i +j]
Notez bien que, 1à encore, la notation désignant un élément d'un tel
tableau est une lvalue. Il n'en ira toutefois pas de même de notations
telles que t[3] ou t[j] bien que,comme nous le verrons un peu plus tard,
de telles notations aient un sens en C.

Le langage C 25

Aucune limitation ne pèse sur le nombre d'indices que peut comporter un
tableau. Seules les limitations de taille mémoire liées à un environ-
nement donné risquent de se faire sentir.

7.2 INITIALISATION DES TABLEAUX
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Il est possible,comme on le fait pour une variable scalaire, d'initialiser
(partiellement ou totalement) un tableau lors de sa déclaration.
Cette fois, cependant, les valeurs fournies devront obligatoirement
être des expressions constantes, et cela quelle que soit la classe
d'allocation du tableau concerné (alors que les variables scalaires
automatiques pouvaient être initialisées avec des expressions quel-
conques).
Voici quelques exemples vous montrant comment initialiser un tableau.

7.2.1 NOTION DE POINTEUR - LES OPÉRATEURS * ET &
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7.2.2 Introduction
~~~~~~~~~~~~~~~~~~
Nous avons déjà été amené à utiliser l'opérateur & pour désigner l'adresse
d'une lvalue. D'une manière générale, le langage C permet de manipuler
des adresses par l'intermédiaire de variables nommées "pointeurs".
En guise d'introduction à cette nouvelle notion,
considérons les instructions:
int * ad;
int n;
n = 20;
ad = &n;
*ad = 30;
La première réserve une variable nommée ad comme étant un "pointeur" sur
des entiers. Nous verrons que * est un opérateur qui désigne le contenu
de l'adresse qui le suit.
Ainsi, à titre "mnémonique", on peut dire que cette déclaration signifie
que *ad, c'est-à-dire l'objet d'adresse ad, est de type int; ce qui
signifie bien que ad est l'adresse d'un entier.
L'instruction:
ad = &n;
affecte à la variable ad la valeur de l'expression &n.
L'opérateur & (que nous avons déjà utilisé avec scanf) est un opérateur
unaire qui fournit comme résultat l'adresse de son opérande.
L'instruction place dans la variable ad l'adresse de la variable n.
L'instruction suivante:
*ad = 30;
signifie: affecter à la lvalue *ad la valeur 30. Or *ad représente
l'entier ayant pour adresse ad (notez bien que nous disons l'entier" et
pas simplement la "valeur" car, ne l'oubliez pas, ad est un pointeur sur
des entiers). Après exécution de cette instruction, la situation est la
suivante:
Bien entendu, ici, nous aurions obtenu le même résultat avec:
n = 30;

7.2.3 Quelques exemples
~~~~~~~~~~~~~~~~~~~~~~~~
Voici quelques exemples d'utilisation de ces deux opérateurs.
Supposez que nous ayons effectué ces déclarations:
int * ad1, * ad2, * ad;
int n = 10, p = 20;
30

Le langage C 26

Les variables ad1, ad2 et ad sont donc des pointeurs sur des entiers.
Considérons maintenant ces instructions:
ad1 = &n; ad2 = &p;
* ad1 = * ad2 + 2;
Les deux premières placent dans ad1 et ad2 les adresses de n et p.
La troisième affecte à *ad1 la valeur de l'expression : *ad2 + 2
Autrement dit, elle place à l'adresse désignée par ad1 la valeur (entière)
d'adresse ad2, augmentée de 2. Cette instruction joue donc ici le même rôle
que : n=p+2;
De manière comparable, l'expression : *ad1 +=3 jouerait le même rôle
que : n = n + 3 et l'expression :
( * ad1 ) ++ jouerait le même rôle que n++
(nous verrons plus loin que, sans les parenthèses, cette expression aurait
une signification différente).
Remarques :
1) Si ad est un pointeur, les expressions ad et *ad sont des lvalue;
autrement dit ad et *ad sont modifiables. En revanche, il n'en va pas de
même de &ad. En effet, cette expression désigne, non plus une variable
pointeur comme ad , mais l'adresse de la variable ad telle qu'elle a été
définie par le compilateur.
Cette adresse est nécessairement fixe et il ne saurait être question de
la modifier (la même remarque s'appliquerait à &n, où n serait une variable
scalaire quelconque).
2) Une déclaration telle que:
int * ad
réserve un emplacement pour un pointeur sur un entier.
Elle ne réserve pas en plus un emplacement pour un tel entier.
Cette remarque prendra encore plus d'acuité lorsque les "objets pointés"
seront des chaînes ou des tableaux.

7.2.4 Incrémentation de pointeurs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Jusqu'ici, nous nous sommes contenté de manipuler, non pas les variables
pointeurs elles mêmes, mais les valeurs pointées. Or si une variable
pointeur ad a été déclarée ainsi : int * ad
une expression telle que : ad+ 1 a un sens pour C.
En effet, ad est censée contenir l'adresse d'un entier et, pour C,
l'expression ci-dessus représente l'adresse de l'entier suivant.
Certes, dans notre exemple, cela n'a guère d'intérêt car nous ne savons
pas avec certitude ce qui se trouve à cet endroit.
Mais nous verrons que cela s'avérera fort utile dans le traitement de
tableaux ou de chaînes.
Notez bien qu'il ne faut pas confondre un pointeur avec un nombre entier.
En effet, l'expression ci-dessus ne représente pas l'adresse de ad
augmentée de un (octet).
Précisément,la différence entre ad+l et ad est ici de sizeof(int) octets.
Si ad avait été déclarée par : double * ad ;cette différence serait de
sizeof(double) octets.
De manière comparable, l'expression : ad++
incrémente l'adresse contenue dans ad de manière qu'elle désigne l'objet
suivant.
Notez bien que des expressions telles que ad+1 ou ad++ sont, en général,
valides, quelle que soit l'information se trouvant réellement à l'emplace-
ment correspondant. D'autre part, il est possible d'incrémenter ou de
décrémenter un pointeur de n'importe quelle quantité entière.

Le langage C 27

Remarque:
Il existera une exception à ces possibilités, à savoir le cas des
pointeurs sur des fonctions, dont nous parlerons plus loin (vous pouvez
dès maintenant comprendre qu'incrémenter un pointeur d'une quantité
correspondant à la taille d'une fonction n'a pas de sens en soi !).

7.3 COMMENT SIMULER UNE TRANSMISSION PAR ADRESSE AVEC UN POINTEUR
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons vu que le mode de transmission par valeur semblait interdire
à une fonction de modifier la valeur de ses arguments effectifs et nous
avions mentionné que les pointeurs fourniraient une solution à ce problème.
Nous sommes maintenant en mesure d'écrire une fonction effectuant la
permutation des valeurs de deux variables.
Voici un programme qui réalise cette opération avec des valeurs entières :

#include <stdio.h>
int main(void)

{ void echange (int * ad1, int * ad2) ;
int a=10, b=20 ;
printf ("avant appel %d %d\n", a, b) ;
echange (&a, &b) ;
printf ("après appel %d %d", a, b) ; avant appel 10 20
} après appel 20 10

void echange (int * ad1, int * ad2)
{
int x ;
x = * ad1 ;
* ad1 = * ad2 ;
* ad2 = x ;
}

Les arguments effectifs de l'appel de echange sont, cette fois, les
adresses des variables n et p (et non plus leurs valeurs).
Notez bien que la transmission se fait toujours par valeur, à savoir
que l'on transmet à la fonction echange les valeurs des
expressions &n et &p.
Voyez comme, dans echange, nous avons indiqué, comme arguments muets,
deux variables pointeurs destinées à recevoir ces adresses.
D'autre part, remarquez bien qu'il n'aurait pas. fallu se contenter
d'échanger simplement les valeurs de ces arguments en écrivant
(par analogie avec la fonction echange du chapitre précédent):
int * x ; x = ad1 ; ad1 = ad2 ; ad2 = x ;
Cela n'aurait conduit qu'à échanger (localement) les valeurs de ces
deux adresses alors qu'il a fallu échanger les valeurs situées à
ces adresses.

7.4 UN NOM DE TABLEAU EST UN POINTEUR CONSTANT
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
En langage C, l'identificateur d'un tableau, lorsqu'il est employé seul
(sans indices à sa suite), est considéré comme un pointeur (constant)
sur le début du tableau.
Nous allons en examiner les conséquences en commençant par le cas des
tableaux à un indice; nous verrons en effet que, pour les tableaux à
plusieurs indices, il faudra tenir compte du type exact du pointeur
en question.

7.4.1 Cas des tableaux à un indice
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Supposons, par exemple, que l'on effectue la déclaration suivante :
int t[10]
La notation t est alors totalement équivalente à &t[0].
L'identificateur t est considéré comme étant de type pointeur sur le
type correspondant aux éléments du tableau, c'est-à-dire, ici,
int *.
Ainsi, voici quelques exemples de notations équivalentes:
t + 1 &t[1]
t + i &t[i]

Le langage C 28

t[i] * (t+i)
Pour illustrer ces nouvelles possibilités de notation, voici plusieurs
façons de placer la valeur 1 dans chacun des 10 éléments de notre
tableau t:
int i ;
for (i=0 ; i<10 ; i++)
*(t+i)=1;
int i ;
int *p;
for(p=t,i=0; i<10 ; i++,p++)
* p = 1;
Dans la seconde façon, nous avons dû recopier la "valeur" représentée
par t dans un pointeur nommé p. En effet, il ne faut pas perdre de vue
que le symbole t représente une adresse constante
( t est une constante de type pointeur sur des entiers).
Autrement dit, une expression telle que t++ aurait été invalide, au
même titre que, par exemple, 3++.
Un nom de tableau est un pointeur constant; ce n'est pas une lvalue.
Remarque importante:
Nous venons de voir que la notation t[i] est équivalente à *(t+i)
lorsque t est déclaré comme un tableau. En fait, cela reste vrai,
quelle que soit la manière dont t a été déclaré.
Ainsi, avec: int * t;
les deux notations précédentes resteraient équivalentes.
Autrement dit,on peut utiliser t[i] dans un programme où t est simplement
déclaré comme un pointeur(encore faudra-t-il, toutefois,avoir alloué
"l'espace mémoire" nécessaire).

7.4.2 Cas des tableaux à plusieurs indices
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comme pour les tableaux à un indice, l'identificateur d'un tableau,
employé seul, représente toujours son "adresse" de début.
Toutefois, si l'on s'intéresse à son type exact, il ne s'agit plus
d'un pointeur sur des éléments du tableau.
En pratique, ce point n'a d'importance que lorsque l'on effectue des
calculs arithmétiques avec ce pointeur (ce qui est assez rare) ou
lorsque l'on doit transmettre ce pointeur en argument d'une fonction;
dans ce dernier cas, cependant, nous verrons que le problème est
automatiquement résolu par la mise en place de conversions, de sorte
qu'on peut ne pas s'en préoccuper.
A simple titre indicatif, nous vous présentons ici les règles employées
par C, en nous limitant au cas de tableaux à deux indices.
Lorsque le compilateur rencontre une déclaration telle que: int t[3] [4];
il considère en fait que t désigne un tableau de 3 éléments,chacun de ces
éléments étant lui même un tableau de 4 entiers.
Autrement dit,si t représente bien l'adresse de début de notre tableau t,
il n'est plus de type int * (comme c'était le cas pour un tableau à un
indice) mais d'un type "pointeur sur des blocs de 4 entiers",
type qui devrait se noter théoriquement4:
int [4] *
Dans ces conditions, une expression telle que t+l correspond à l'adresse
de t, augmentée de 4 entiers (et non plus d'un seul !).
Ainsi, les notations t et &t[0][0] correspondent toujours à la même
adresse, mais l'incrémentation de 1 n'a pas la même signification pour
les deux.
D'autre part, les notations telles que t[0], t[1] ou t[i] ont un sens.
Par exemple, t[0] représente l'adresse de début du premier bloc
(de 4 entiers) de t, t[l], celle du second bloc... Cette fois,
il s'agit bien de pointeurs de type int *.

Le langage C 29

Autrement, dit les notations suivantes sont totalement équivalentes
(elles correspondent à la même adresse et elles sont de même type):
t[0] &t[0][0]
t[l] &t[11[0]

7.5 LES TABLEAUX TRANSMIS EN ARGUMENT
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lorsque l'on place le nom d'un tableau en argument effectif de l'appel
d'une fonction,on transmet finalement l'adresse du tableau à la fonction,
ce qui lui permet d'effectuer toutes les manipulations voulues sur ses
éléments, qu'il s'agisse d'utiliser leur valeur ou de la modifier.
Voyons quelques exemples pratiques.

7.5.1 Cas des tableaux à un indice
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a) Premier exemple: tableau de taille fixe
Voici un exemple de fonction qui met la valeur 1 dans tous les
éléments d'un tableau de 10 éléments, l’ adresse de ce tableau étant
transmise en argument.
void fct (int t[10])
{int i ;
for (i=0 ; i<10 ; i++) t[i] =1 ;
}

Voici deux exemples d'appels possibles de cette fonction :
int t1[10], t2[10] ;
.....
fct(t1) ;
....
fct(t2) ;
L'en-tète de f peut être indifféremment écrit de l'une des manières
suivantes:
void fct (int t[10]) void fct (int * t) void fct (int t[])

b) Second exemple: tableau dont le nombre d'éléments est variable
Comme nous venons de le voir,lorsqu'un tableau à un seul indice apparaît
en argument d'une fonction,le compilateur n'a pas besoin d'en connaître
la taille exacte. Il est ainsi facile de réaliser une fonction capable
de travailler avec un tableau de dimension quelconque,à condition de lui
en transmettre la taille en argument. Voici, par exemple, une fonction
qui calcule la somme des éléments d'un tableau d'entiers de taille
quelconque:
int som (int t[],int nb)
{ int s = 0, i ;
for (i=0 ; i<nb ; i++)
s += t[i] ;
return (s) ;
}

Voici quelques exemples d'appels de cette fonction:
int main(void)
{ int t1[30], t2[15], t3[10] ;
int s1, s2, s3 ;
.....
s1 = som(t1, 30) ;
s2 = som(t2, 15) + som(t3, 10) ;
.....
}

7.5.2 Cas des tableaux à plusieurs indices
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a) Premier exemple: tableau de taille fixe
Voici un exemple d'une fonction qui place la valeur 1 dans chacun des
éléments d'un tableau de dimensions 10 et 15

Le langage C 30

void raun (int t[10][15])
{ int i, j ;
for (i=0 ; i<10 ; i++)
for (j=0 ; j<15 ; j++) t[i][j] = 1 ;
}
b) Second exemple: tableau de dimensions variables
Supposons que nous cherchions à écrire une fonction qui place la
valeur 0 dans chacun des éléments de la diagonale d'un tableau carré
de taille quelconque. Une façon de résoudre ce problème consiste à
"adresser'' les éléments voulus par des pointeurs en effectuant
le ''calcul d'adresse approprié".
void diag (int * p, int n)
{ int i ;
for (i=0; i<n; i++)
{
* p = 0 ;
p += n+1;
}
}
Notre fonction reçoit donc, en premier argument,l'adresse du premier
élément du tableau, sous forme d'un pointeur de type int *.
Ici, nous avons tenu compte de ce que deux éléments consécutifs de la
diagonale sont séparés par n éléments.
Si, donc, un pointeur désigne un élément de la diagonale, pour "pointer"
sur le suivant il suffit d'incrémenter ce pointeur de n+1 unités
(l'unité étant ici la taille d'un entier).
Remarques:
1) Un appel de notre fonction diag se présentera ainsi:
int t [30] [30] ;
diag (t, 30)
Or l'argument effectif t est, certes, l'adresse de t, mais d'un type
pointeur sur des blocs de 10 entiers et non pointeur sur des entiers.
En fait, la présence d'un prototype pour diag fera qu'il sera converti
en un int *.
2) Cette fonction pourrait également s'écrire en y déclarant un tableau à
une seule dimension dont la taille (n*n) devrait alors être fournie en
argument (en plus de n).
Le même mécanisme d'incrémentation de n+1 s'appliquerait alors,non plus
à un pointeur, mais à la valeur d'un indice.

7.6 Exercices
~~~~~~~~~~~~~
1) Ecrire, de deux façons différentes, un programme qui lit 10 nombres entiers dans un tableau avant d'en rechercher le plus grand et le plus petit:
a) en utilisant uniquement le "formalisme tableau",
b) en utilisant le "formalisme pointeur", chaque fois que cela est possible.

2) Ecrire une fonction qui ne renvoie aucune valeur et qui détermine la
valeur maximale et la valeur minimale d'un tableau d'entiers
(à un indice) de taille quelconque.
Il faudra donc prévoir 4 arguments: le tableau, sa dimension, le maximum
et le minimum.
Ecrire un petit programme d'essai.
3) Ecrire une fonction permettant de trier par ordre croissant les valeurs
entières d'un tableau de taille quelconque (transmise en argument).
Le tri pourra se faire par réarrangement des valeurs au sein du tableau
lui-même.
4) Ecrire une fonction calculant la somme de deux matrices dont les éléments
sont de type double. Les adresses des trois matrices et leurs dimensions
(communes) seront transmises en argument.


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\08. LES CHAINES DE CARACTÈRES.rtf

8. LES CHAINES DE CARACTÈRES
8.1 REPRÉSENTATiON DES CHAINES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8.1.1 La convention adoptée
~~~~~~~~~~~~~~~~~~~~~~~~~~~
En C, une chaîne de caractères est représentée par une suite d'octets
correspondant à chacun de ses caractères (plus précisément à chacun de
leurs codes), le tout étant terminé par un octet supplémentaire de code
nul.Cela signifie que, d'une manière générale,une chaîne de n caractères
occupe en mémoire un emplacement de n + 1 octets .

8.1.2 Cas des chaînes constantes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C'est cette convention qu'utilise le compilateur pour représenter les
"constantes chaîne"(sous-entendu que vous introduisez dans vos prgm),
sous des notations de la forme:
'' bonjour ''
De plus, une telle notation sera traduite par le compilo en un pointeur
(sur des éléments de type char) sur la zone mémoire correspondante.
Voici un programme illustrant ces deux particularités:
#include <stdio.h>
int main(void)
{ char * adr ;
adr = "bonjour" ; bonjour
while (*adr)
{printf ("%c",* adr) ;
adr++ ;
}
return 0 ;
}
La déclaration :
char * adr;
réserve simplement l'emplacement pour un pointeur sur un caractère
(ou une suite de caractères). En ce qui concerne la constante :
''bonjour''
le compilateur a créé en mémoire la suite d'octets correspondants mais,
dans l'affectation :
adr = ''bonjour"
la notation ''bonjour'' a comme valeur, non pas la valeur de la chaîne
elle-même, mais son adresse; on retrouve là le même phénomène que pour
les tableaux.

8.1.3 Initialisation de tableaux de caractères
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comme nous l'avons dit, vous serez souvent amené, en C, à placer des
chaînes dans des tableaux de caractères.
Mais, si vous déclarez, par exemple : char ch[20] ; vous ne pourrez pas
pour autant transférer une chaîne constante dans ch, en écrivant une
affectation du genre : ch = ''bonjour'';
En effet, ch est une constante pointeur qui correspond à l'adresse que
le compilateur a attribuée au tableau ch; ce n'est pas une lvalue;
il n'est donc pas question de lui attribuer une autre valeur
(ici, il s'agirait de l'adresse attribuée par le compilateur à la
constante chaîne ''bonjour'') .

Le langage C 32

Par contre, C vous autorise à initialiser votre tableau de caractères à
l'aide d'une chaîne constante. Ainsi, vous pourrez écrire :
char ch[20] = "bonjour’’
Cela sera parfaitement équivalent à une initialisation de ch réalisée
par une énumération de caractères ( et le code zéro - noté \0) :
char ch[20] = { 'b','o','n','j','o','u','r','\0' }
N'oubliez pas que,dans ce dernier cas,les 12 caractères non initialisés
explicitement seront. .

- soit initialisés à zéro (pour un tableau de classe statique):on voit que,
dans ce cas, l'omission du caractère '\0' ne serait (ici) pas grave,
- soit ''aléatoires'' (pour un tableau de classe automatique): dans ce cas, l'omission du caractère '\0' serait nettement plus gênante.

De plus,comme le langage C autorise l'omission de la dimension d'un tableau
lors de sa déclaration,lorsqu'elle est accompagnée d'une initialisation,
il est possible d'écrire une instruction telle que :
char message[] = ''bonjour'';
Celle-ci réserve un tableau, nommé message, de 8 caractères
(compte tenu du caractère'\0')

8.1.4 Initialisation de tableaux de pointeurs sur des chaînes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous avons vu qu'une chaîne constante était traduite par le compilateur
en une adresse que l'on pouvait, par exemple, affecter à un pointeur
sur une chaîne. Cela peut se généraliser à un tableau de pointeurs,
comme dans:

char * jour[7] = {"lundi", "mardi", "mercredi", "jeudi",
"vendredi", "samedi", "dimanche" } ;

Cette déclaration réalise donc à la fois la création des 7 chaînes
constantes correspondant aux 7 jours de la semaine et l'initialisation
du tableau jour avec les 7 adresses de ces 7 chaînes.
Voici un exemple employant cette déclaration (nous y avons fait appel,
pour l'affichage d'une chaîne, au code de format %s, dont nous repar-
lerons un peu plus loin) :

#include <stdio.h>
int main(void)
{ char * jour[7] = { "lundi", "mardi", "mercredi", "jeudi",
"vendredi", "samedi", "dimanche" } ;
int i ;
printf ("donnez un entier entre 1 et 7 : ") ;
scanf ("%d", &i) ;
printf ("Le jour numéro °/d de la semaine est %s", i, jour[i-1] ) ;
return 0 ;
}
donnez un entier entre 1 et 7 : 3
le jour numéro 3 de la semaine est mercredi
Remarque: la situation présentée ne doit pas être confondue avec la
précédente. Ici, nous avons affaire à un tableau de sept pointeurs,
chacun d'entre eux désignant une chaîne constante .

8.2 POUR LIRE ET ÉCRIRE DES CHAINES

Le langage C à plusieurs possibilités de lecture ou d'écriture de chaînes:
- l'utilisation du code de format %s dans les fonctions printf et scanf,
- les fonctions spécifiques de lecture (gets) ou d'affichage (puts) d'une
chaîne (une seule à la fois).
Voyez cet exemple de programme:
#include <stdio.h>

Le langage C 33

int main(void)
{ char nom[2O], prenom[20], ville[25] ;
printf ("quelle est votre ville : ") ;
gets (ville) ;
printf ("donnez votre nom et votre prénom : ") ;
scanf ("%s %s", nom, prenom) ;
printf ("bonjour cher %s %s qui habitez à ", prenom, nom) ;
puts (ville) ;
return 0 ;
}
quelle est votre ville : Paris
donnez votre nom et votre prénom : Dupont Yves
bonjour cher Yves Dupont qui habitez à Paris

Les fonctions printf et scanf permettent de lire ou d'afficher simultané-
ment plusieurs informations de type quelconque.
Par contre, gets et puts ne traitent qu'une chaîne à la fois.
De plus, la délimitation de la chaîne lue ne s'effectue pas de la même
façon avec scanf et gets. Plus précisément :
- avec le code %s de scanf, on utilise les délimiteurs "habituels"
(l'espace ou la fin de ligne). Cela interdit donc la lecture d'une chaîne
contenant des espaces. De plus, le caractère délimiteur n'est pas
"consommé" : il reste disponible pour une prochaine lecture ;
- avec gets, seule la fin de ligne sert de délimiteur.
De plus, contrairement à ce qui se produit avec scanf, ce caractère est
effectivement "consommé": il ne risque pas d'être pris en compte lors
d'une nouvelle lecture.
Dans tous les cas, vous remarquerez que la lecture de n caractères implique
le stockage en mémoire de n + 1 caractères , car le caractère de fin de
chaîne (\0) est généré automatiquement par toutes les fonctions de lecture
(notez toutefois que le caractère séparateur-fin de ligne ou autre-n'est
pas recopié en mémoire).
Ainsi, dans notre précédent prgm, il n'est pas possible (du moins pas
souhaitable!) que le nom fourni en donnée contienne plus de 19 caractères.
Remarques:
1) Dans les appels des fonctions scanf et puts, les identificateurs de
tableau comme nom, prenom ou ville n'ont pas besoin d'être précédés de
l'opérateur & puisqu'ils représentent déjà des adresses.
2) La fonction gets fournit en résultat soit un pointeur sur la chaîne lue
(c'est donc en fait la valeur de son argument), soit le pointeur nul en
cas d'anomalie.
3) La fonction puts réalise un changemnt de ligne à la fin de l'affichage de
la chaîne, ce qui n'est pas le cas de printf avec le code de format %s.
4) Nous nous sommes limité ici aux entrées-sorties "conversationnelles".
Les autres, .. seront examinées dans le chapitre consacré aux fichiers.
5) Si, dans notre précédent programme, l'utilisateur introduit une fin de
ligne entre le nom et le prénom, la chaîne affectée à prenom n'est rien
d'autre que... la chaîne vide ! Ceci provient de ce que la fin de ligne
servant de délimiteur pour le premier %s n'est pas consommée et se
trouve donc reprise par le %s suivant...
6) Compte tenu de ce que gets consomme la fin de ligne servant de délimiteur,
alors que le code %s de scanf ne le fait pas, il n'est guère possible,
dans le programme précédent, d'inverser les utilisations de scanf et de
gets (en lisant la ville par scanf puis le nom et le prénom par scanf):
dans ce cas, la fin de ligne non consommée par scanf amènerait gets à
introduire une chaîne vide comme nom.
D'une manière générale,d'ailleurs,il est préférable,autant que possible,
de faire appel à gets plutôt qu'au code %s pour lire des chaînes.

8.3 GÉNÉRALITÉS SUR LES FONCTIONS PORTANT SUR DES CHAINES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C dispose de nombreuses fonctions de manipulation de chaînes.
Avant d'en voir les principales (les autres étant, de toute façon,
présentées dans l'annexe A), voyons quelques principes généraux.
La fonction strlen fournit en résultat la longueur d’une chaîne dont
on lui a transmis l’adresse en valeur.

Le langage C 34

La fonction strcat(ch1,ch2) <string.h> recopie la seconde chaîne ch2 à la
suite de la première ch1.
La fonction strncat(ch1, ch2, lgmax) <string.h> travaille de la même façon
que strcat en offrant un contrôle sur le nombre de caractères qui seront
concaténés à la chaine ch2.
La fonction strcmp(ch1, ch2) <string.h> compare deux chaînes et fournit une
- valeur entière positive si ch1>ch2,
- nulle si ch1=ch2 et négative si ch1<ch2.
La fonction strncmp(ch1,ch2,lgmax) <string.h> travaille comme strcmp mais
elle limite la comparaison au nombre lgmax de caractères.
Les fonctions stricmp(ch1, ch2) et strnicmp(ch1, ch2, lgmax) <string.h>
travaillent comme strcmp et strncmp mais sans tenir compte de la diffé-
rence entre majuscules et minuscules.
La fonction strcpy(destin, source) <string.h> recopie la chaîne source
dans l’emplacement d’adresse destin.
La fonction strncpy(destin, source, lgmax) <string.h> limite la copie au
nombre de caractères lgmax.
La fonction strchr(ch,caractère) <string.h> recherche dans ch, la première
position où apparait le caractère mentionné.
La fonction strrchr(ch,caractère) <string.h> opère de même mais en partant
de la fin de ch;
La fonction strstr(ch,ssch) <string.h> recherche dans ch la première occur-
rence de la sous chaîne ssch.

8.4 Exercices
~~~~~~~~~~~~~~
1) Ecrire un programme déterminant le nombre de lettres e (minuscules)
présentes dans un texte de moins d'une ligne (supposée ne pas dépasser
132 caractères) fourni au clavier.
2) Ecrire un programme qui supprime toutes les lettres e (minuscules)
d'un texte de moins d'une ligne (supposée ne pas dépasser 132 caractères)
fourni au clavier.
Le texte ainsi modifié sera créé, en mémoire, à la place de l'ancien.
3) Ecrire un programme qui lit au clavier un mot (d'au plus 30 caractères)
et qui l'affiche "à l'envers '' .
4) Ecrire un programme qui lit un verbe du premier groupe et qui en affiche
la conjugaison au présent de l'indicatif, sous la forme :
je chante
tu chantes
il chante
nous chantons
vous chantez
ils chantent
Le programme devra vérifier que le mot fourni se termine bien par ''er".
On supposera qu'il ne peut comporter plus de 26 lettres et qu'il s'agit
d'un verbe régulier.
Autrement dit, on admettra que l'utilisateur ne fournira pas un verbe tel
que "manger" (le programme afficherait alors : ''nous mangons" !).

Le langage C 35


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\09. LES STRUCTURES.rtf

9. LES STRUCTURES
Nous avons déjà vu comment le tableau permettait de désigner sous un seul
nom un ensemble de valeurs de même type, chacune d'entre elles étant
repérée par un indice.
La structure, quant à elle, va nous permettre de désigner sous un seul
nom un ensemble de valeurs pouvant être de types différents.
L'accès à chaque élément de la structure (nommé champ) se fera, cette
fois, non plus par une indication de position, mais par son nom au
sein de la structure.

9.1 DÉCLARATION D'UNE STRUCTURE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Voyez tout d'abord cette déclaration :
struct enreg
{ int numero;
int qte;
double prix;
}
Celle-ci définit un modèle de structure mais ne réserve pas de variables
correspondant à cette structure. Ce modèle s'appelle ici enreg et il
précise le nom et le type de chacun des "champs" constituant la structure
(numero, qte et prix).
Une fois un tel modèle défini,nous pouvons déclarer des "variables" du
type correspondant (souvent, nous parlerons de structure pour désigner
une variable dont le type est un modèle de structure).
Par exemple : struct enreg art1 ;
réserve un emplacement nommé artl ''de type enreg'' destiné à contenir
deux entiers et un double.
De manière semblable : struct enreg art1, art2 ;
réserverait deux emplacements artl et art2 du type enreg.

9.2 UTILISATION D'UNE STRUCTURE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
En C, comme en Pascal, il est possible d'utiliser une structure de deux
manières :
- en travaillant individuellement sur chacun de ses champs,
- en travaillant de manière "globale'' sur l'ensemble de la structure.

9.2.1 Utilisation des champs d'une structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Chaque champ d'une structure peut être manipulé comme n'importe quelle
variable du type correspondant.
La désignation d'un champ se note en faisant suivre le nom de la variable
structure de l'opérateur ''point'' (.) suivi du nom de champ tel qu'il
a été défini dans le modèle
(le nom de modèle lui-même n'intervenant d'ailleurs pas).
Voici quelques exemples utilisant le modèle enreg et les variables artl
et art2 déclarées de ce type.
artl.numero = 15 ;
affecte la valeur 15 au champ numero de la structure artl.
printf ("%e", artl.prix) ; affiche, suivant le code format %e,
la valeur du champ prix de la structure artl.
scanf ("%e", &art2.prix) ;lit, suivant le code format %e,
une valeur qui sera affectée au champ prix de la structure art2.
Notez bien la présence de l'opérateur &.
artl.numero++ incrémente de 1 la valeur du champ numero de la structure
artl.

Le langage C 36

Remarque : la priorité de l'opérateur ''.'' est très élevée, de sorte
qu'aucune des expressions ci-dessus ne nécessite de parenthèses.

9.2.2 Utilisation globale d'une structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Il est possible d'affecter à une structure le contenu d'une structure
définie à partir du même modèle.
Par exemple, si les structures artl et art2 ont été déclarées suivant
le modèle enreg défini précédemment, nous pourrons écrire :
artl = art2;
Une telle affectation globale remplace avantageusement :
art1.numero = art2.numero ; art1.qte = art2.qte ; art1.prix = art2.prix ;
Notez bien qu'une affectation globale n'est possible que si les
structures ont été définies avec le même nom de modèle; en particulier,
elle sera impossible avec des variables ayant une structure analogue
mais définies sous deux noms différents.
L'opérateur d'affectation et, comme nous le verrons un peu plus loin,
l'opérateur d'adresse & sont les seuls opérateurs s'appliquant à une
structure (de manière globale).
Remarque:
L'affectation globale n'est pas possible entre tableaux.
Elle l'est, par contre, entre structures. Aussi est-il possible,en créant
artificiellement une structure contenant un seul champ qui est un tableau, de réaliser une affectation globale entre tableaux.

9.2.3 Initialisations de structures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On retrouve pour les structures les règles d'initialisation qui sont en
vigueur pour tous les types de variables, à savoir:
- En l'absence d'initialisation explicite,
les structures de classe ''statique" sont,par défaut,initialisées à zéro;
celles possédant la classe "automatique'' ne sont pas initialisées par
défaut (elles contiendront donc des valeurs aléatoires).
- Il est possible d'initialiser explicitement une structure lors de sa
déclaration. On ne peut toutefois employer que des constantes ou des
expressions constantes et cela aussi bien pour les structures statiques
que pour les structures automatiques (alors que,les variables scalaires
automatiques, il était possible d'employer une expression quelconque):
Voici un exemple d'initialisation de notre structure artl, au moment de
sa déclaration:
struct enreg art1 = { 100, 285, 2000 };
Vous voyez que la description des différents champs se présente sous la
forme d'une liste de valeurs séparées par des virgules, chaque valeur
étant une constante ayant le type du champ correspondant.
Là encore, il est possible d'omettre certaines valeurs.

9.3 POUR SIMPLIFIER LA DÉCLARATION DE TYPES DÉFINIR DES SYNONYMESavecTYPEDEF
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La déclaration typedef permet de définir ce que l'on nomme en langage C
des types synonymes.
A priori,elle s'applique à tous les types et pas seulement aux structures.
C'est pourquoi nous commencerons par l'introduire sur quelques exemples
avant de montrer l'usage que l'on peut en faire avec les structures.

9.3.1 Exemples d'utilisation de typedef
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Le langage C 37

La déclaration : typedef int entier;
signifie que entier est ''synonyme'' de int,de sorte que les déclarations
suivantes sont équivalentes:
int n, p ; entier n, p ;

9.3.2 Application aux structures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
En faisant usage de typedef,les déclarations des structures artl et art2 du
paragraphe 1 peuvent être réalisées comme suit:
struct enreg
{ int numero ;
int qte ;
double prix ;
}
typedef struct enreg s_enreg ;
s_enreg art1, art2

ou encore, plus simplement:
typedef struct
{ int numero ;
int qte ;
float prix ;
} s_enreg ;
s_enreg art1, art2 ;
Par la suite, nous ne ferons appel qu'occasionnellement à typedef, afin de
ne pas vous enfermer dans un style de notations que vous ne retrouverez
pas nécessairement dans les programmes que vous serez amené à utiliser.

9.4 IMBRICATION DE STRUCTURES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Dans nos exemples d'introduction des structures, nous nous sommes limité à
une structure simple ne comportant que trois champs d'un type de base.
Mais chacun des champs d'une structure peut être d'un type absolument
quelconque: pointeur, tableau, structure,... De même, un tableau peut
être constitué d'éléments qui sont eux-mêmes des structures.
Voyons ici quelques situations classiques.

9.4.1 Structure comportant des tableaux
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Soit la déclaration suivante:
struct personne { char nom[30] ;
char prenom [20] ;
double heures [31] ;
} employe, courant ;
Celle-ci réserve les emplacements pour deux structures nommées employe
et courant. Ces dernières comportent trois champs:

- nom qui est un tableau de 30 caractères,
- prenom qui est un tableau de 20 caractères,
- heures qui est un tableau de 31 flottants.

Le langage C 38

On peut, par exemple, imaginer que ces structures permettent de conserver
pour un employé d'une entreprise les informations suivantes:
- prenom,
- nombre d'heures de travail pendant chacun des jours du mois courant.

La notation:
employe.heures[4]
désigne le cinquième élément du tableau heures de la structure employe.
Il s'agit d'un élément de type float.
Notez que, malgré les priorités identiques des opérateurs . et [],
leur associativité de gauche à droite évite l'emploi de parenthèses.
De même:
employe.nom[0]
représente le premier caractère du champ nom de la structure employe.
Par ailleurs:
&courant.heures[4]
représente l'adresse du cinquième élément du tableau heures de la struc-
ture courant.
Notez que, la priorité de l'opérateur & étant inférieure à celle des deux
autres, les parenthèses ne sont, 1à encore, pas nécessaires.
Enfin:
courant.nom
représente le champ nom de la structure courant, c'est-à-dire plus
précisément l'adresse de ce tableau.
A titre indicatif, voici un exemple d'initialisation d'une structure de
type personne lors de sa déclaration:
struct personne emp = { "Dupont", "Jules", { 8, 7, 8, 6, 8, 0, 0, 8}}

9.4.2 Tableaux de structures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Voyez ces déclarations :
struct point { char nom ;
int x ;
int y ;
};
struct point courbe[50];
La structure point pourrait, par exemple, servir à représenter un
point d'un plan, point qui serait défini par son nom (caractère) et
ses deux coordonnées.
La structure courbe, quant à elle, pourrait servir à représenter un
ensemble de 50 points du type ainsi défini.
Notez bien que point est un nom de modèle de structure, tandis que
courbe représente effectivement un ''objet'' de type "tableau de 50
éléments du type point".
Si i est un entier, la notation : courbe[i].nom représente le nom du
point de rang i du tableau courbe. Il s'agit donc d'une valeur de
type char. Notez bien que la notation : courbe.nom[i] pas de sens.
De même, la notation : courbe[i].x représente la valeur du champ x de
l’élément de rang i du tableau courbe.

9.5 À PROPOS DE LA PORTÉE DU MODÈLE DE STRUCTURE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A l'image de ce qui se produit pour les identificateurs de variables,
la "portée" d'un modèle de structure dépend de l'emplacement de sa
déclaration:
attention : rajouter extern _turboFloat après les include et
(void)_turboFloat n’importe ou dans le main() (Bug Borland C++)

Le langage C 39

- si elle se situe au sein d'une fonction (y compris,la "fonction main"),
elle n'est accessible que depuis cette fonction,
- si elle se situe en dehors d'une fonction, elle est accessible de toute
la partie du fichier source qui suit sa déclaration;
elle peut ainsi être utilisée par plusieurs fonctions.
Il est néanmoins toujours possible de placer un certain nombre de
déclarations de modèles de structures dans un fichier séparé que l'on
incorpore par #include à tous les fichiers source où l'on en a besoin.
Cette méthode évite la duplication des déclarations identiques avec les
risques d'erreurs qui lui sont inhérents.

9.6 TRANSMISSION D'UNE STRUCTURE EN ARGUMENT D'UNE FONCTION
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Jusqu'ici, nous avons vu qu'en C la transmission des argument se fait
"par valeur", ce qui implique une recopie de l'information transmise à
la fonction. Par ailleurs, il est toujours possible de transmettre
la "valeur d'un pointeur" sur une variable, auquel cas la fonction peut,
si besoin est, en modifier la valeur. Ces remarques s'appliquent également
aux structures (notez qu'il n'en allait pas de même pour un tableau,dans
la mesure où la seule chose qu'on puisse transmettre dans ce cas soit la
valeur de l'adresse de ce tableau).

9.6.1 Transmission de la valeur d'une structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Aucun problème particulier ne se pose.
Il s'agit simplement d'appliquer ce que nous connaissons déjà.
Voici un exemple simple:

#include <stdio.h>
struct enreg { int a;
double b;
}
int main(void)
{
struct enreg x;
void fct (struct enreg y);
x.a = 1; x.b = 12.5;
printf ("\navant appet fct: %d %e",x.a,x.b);
fct (x);
printf ("\n au retour dans main: %d %e", x.a, x.b);
}
void fct (struct enreg s)
{
s.a = 0;
s.b=1;
printf ("\ndans fct: %d %e", s.a, s.b);
}

avant appel fct : 1 1.25000e+01
dans fct : 0 1.00000e+00
au retour dans main : 1 1.25000e+01
Naturellement, les valeurs de la structure x sont recopiées localement
dans la fonction fct lors de son appel; les modifications de s au sein
de fct n'ont aucune incidence sur les valeurs de x.

9.6.2 Transmission de l'adresse d'une structure: l'opérateur ->
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cherchons à modifier notre précédent programme pour que la fonction fct
reçoive effectivement l'adresse d'une structure et non plus sa valeur.
L'appel de fct devra donc se présenter sous la forme :
fct (&x) ;
Cela signifie que son en-tête sera de la forme :
void fct (struct enreg * ads) ;
Comme vous le constatez, le problème se pose alors d'accéder, au sein de
la définition de fct, à chacun des champs de la structure d'adresse ads.
L'opérateur "." ne convient plus,car il suppose comme premier opérande un
nom de structure et non une adresse.Deux solutions s'offrent alors à vous:

Le langage C 40

- adopter une notation telle que (*ads).a ou (*ads).b pour désigner les
champs de la structure d'adresse ads.
- faire appel à un nouvel opérateur noté -> , lequel permet d'accéder aux
différents champs d'une structure à partir de son adresse de début.
Ainsi, au sein de fct, la notation ads - > b désignera le second champ de
la structure reçue en argument; elle sera équivalente à (*ads).b.
Voici ce que pourrait devenir notre précédent exemple en employant
l'opérateur - > :

#include <stdio.h>
struct enreg { int a;
float b;
}
int main(void)
{ struct enreg x;
void fct (struct enreg *);
x.a = 1; x.b = 12.5;
printf ("\navant appel fct: %d %e",x.a,x.b);
fct (&x);
printf ("\n au retour dans main : %d %e", x.a, x.b);
retunr 0 ;
}
void fct (struct enreg * ads)
{
ads->a = 0;
ads->b = 1;
printf ("\ndans fct: %d %e", ads->a, ads->b);
}
avant appel fct: 1 1.25000e+01
dans fct: 0 1.00000e+00
au retour dans main: 0 1.00000e+00

9.7 Exercices
~~~~~~~~~~~~~~
1) Ecrire un programme qui:

a) lit au clavier des informations dans un tableau de structures du type
point défini comme suit:
struct point {
int num;
double x ;
double y;
}
Le nombre d'éléments du tableau sera fixé par une instruction #define.
b) affiche à l'écran l'ensemble des informations précédentes.
2) Réaliser la même chose que dans l'exercice précédent,mais en prévoyant,
cette fois, une fonction pour la lecture des informations et une fonction
pour l'affichage.

Le langage C 41


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\10. LES FICHIERS.rtf

10. LES FICHIERS
Nous avons déjà eu l'occasion d'étudier les "entrées-sorties conversa-
tionnelles '', c'est-à-dire les fonctions permettant d'échanger des
informations entre le programme et l'utilisateur.
Nous vous proposons ici d'étudier les fonctions permettant au programme
d'échanger des informations avec des "fichiers''.
A priori, ce terme de fichier désigne plutôt un ensemble d'informations
situé sur une ''mémoire de masse'' telle que le disque ou la disquette.
Nous verrons toutefois qu'en C, comme d'ailleurs dans d'autres langages,
tous les périphériques, qu'ils soient d'archivage (disque, disquette,..)
ou de communication (clavier, écran, imprimante,...), peuvent être
considérés comme des fichiers. Ainsi, en définitive, les entrées-sorties
conversationnelles apparaîtront comme un cas particulier de la gestion
de fichiers.

Rappelons que l'on distingue traditionnellement deux techniques de gestion
de fichiers :

- l'accès séquentiel consiste à traiter les informations "séquentiellement",
c'est-à-dire dans l'ordre où elles apparaissent (ou apparaîtront) dans
le fichier,
- l'accès direct consiste à se placer immédiatement sur l'information
souhaitée, sans avoir à parcourir celles qui la précèdent.

En fait, pour des fichiers disque (ou disquette), la distinction entre
accès séquentiel et accès direct n'a plus véritablement de raison d'être.
D'ailleurs, comme vous le verrez, en langage C, vous utiliserez les mêmes
fonctions dans les deux cas (exception faite d'une fonction de déplacement
de pointeur de fichier). Qui plus est, rien ne vous empêchera de mélanger
les deux modes d'accès pour un même fichier.
Cependant, pour assurer une certaine progressivité à notre propos,nous allons
commencer par vous montrer comment travailler de manière séquentielle.

10.1 CRÉATION SÉQUENTIELLE D'UN FICHIER
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Voici un programme qui se contente d'enregistrer séquentiellement dans
un fichier une suite de nombres entiers qu'on lui fournit au clavier.

#include <stdio.h>
int main(void)
{
char nomfich[21] ;
int n ;
FILE * sortie ;
printf ("nom du fichier à créer : ") ;
scanf ("%20s", nomfich) ;
sortie = fopen (nomfich, "w") ;
do{
printf ("donnez un entier : ") ;
scanf ("%d", &n) ;
if (n) fwrite(&n, sizeof(int), 1, sortie) ;
} while (n) ;
fclose (sortie) ;
return 0 ;
}

Nous avons déclaré un tableau de caractères nomfich destiné à contenir,
sous forme d'une chaine, le nom du fichier que l'on souhaite créer.
La déclaration : FILE * sortie; signifie que sortie est un pointeur sur
un objet de type FILE.
Ce nom désigne en fait un modèle de structure défini dans l'fichier stdio.h
(par une instruction typedef, ce qui explique l'absence du mot struct).

Le langage C 42

N'oubliez pas que cette déclaration ne réserve qu'un emplacement pour un
pointeur. C'est la fonction fopen qui créera effectivement une telle
structure et qui en fournira l'adresse en résultat.
La fonction fopen est ce que l'on nomme une fonction d'ouverture de fichier.
Elle possède deux arguments :
- le nom du fichier concerné,fourni sous forme d'une chaîne de caractères;
ici, nous avons prévu que ce nom ne dépassera pas 20 caractères
(le chiffre 21 tenant compte du caractère \0); notez qu'en général ce
nom pourra comporter une information (chemin, répertoire,...) permettant
de préciser l'endroit où se trouve le fichier.
- une indication, fournie elle aussi sous forme d'une chaîne, précisant ce
que l'on souhaite faire avec ce fichier.

Ici, on trouve w (abréviation de write) qui permet de réaliser une
"ouverture en écriture".Plus précisément,si le fichier cité n'existe pas,
il sera créé par fopen. S'il existe déjà, son ancien contenu deviendra
inaccessible.
Autrement dit, après l'appel de cette fonction, on se retrouve dans tous
les cas en présence d'un fichier "vide".
Le remplissage du fichier est réalisé par la répétition de l'appel :

fwrite (&n, sizeof(int), 1, sortie);

La fonction fwrite possède quatre arguments précisant :
- l'adresse d'un bloc d'informations (ici &n),
- la taille d'un bloc, en octets : ici sizeof(int); notez l'emploi de
l'opérateur sizeof qui assure la portabilité du programme,
- le nombre de blocs de cette taille que l'on souhaite transférer dans
le fichier (ici l),
- l'adresse de la structure décrivant le fichier (sortie).
Notez que, d'une manière générale, fwrite permet de transférer plusieurs
blocs consécutifs de même taille à partir d'une adresse donnée.
Enfin, la fonction fclose réalise ce que l'on nomme une "fermeture" de
fichier. Elle force l'écriture sur disque du tampon associé au fichier
Remarques:
1) On emploie souvent le terme flux (en anglais stream) pour désigner un
pointeur sur une structure de type FILE. Ici, par exemple, sortie est
un flux que la fonction fopen aura associé à un certain fichier.
D'une manière générale, par souci de simplification, lorsque aucune
ambiguïté ne sera possible, nous utiliserons souvent le mot fichier à
la place de flux .
2) fopen fournit un pointeur nul en cas d'impossibilité d'ouverture du
fichier. Ce sera le cas, par exemple, si l'on cherche à ouvrir en lecture
un fichier inexistant ou encore si l'on cherche à créer un fichier sur
une disquette saturée.
3) fwrite fournit le nombre de blocs effectivement écrits.
Si cette valeur est inférieure au nombre prévu, cela signifie qu'une
erreur est survenue en cours d'écriture. Cela peut être, par exemple,
une disquette pleine, mais cela peut se produire également lorsque
l'ouverture du fichier s'est mal déroulée (et que l'on n'a pas pris soin
d'examiner le code de retour de fopen).

10.2 LISTE SÉQUENTIELLE D'UN FICHIER
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Voici maintenant un programme qui permet de lister le contenu d'un fichier
quelconque tel qu'il a pu être créé par le programme précédent.

#include <stdio.h>
int main(void)
{
char nomfich[21] ;
int n ;
Le langage C 43
FILE * entree ;
printf ("nom du fichier à lister : ") ;
scanf ("%20s", nomfich) ;
entree = fopen (nomfich, "r") ;
while ( fread (&n, sizeof(int), 1, entree), ! feof(entree) )
{
printf ("\n%d", n) ;
}
fclose (entree) ;
return 0 ;
}

Les déclarations sont identiques à celles du programme précédent.
En revanche, on trouve cette fois, dans l'ouverture du fichier,
l'indication r (abréviation de read).
Elle précise que le fichier en question ne sera utilisé qu'en lecture.
Il est donc nécessaire qu'il existe déjà (nous verrons un peu plus loin
comment traiter convenablement le cas où il n'existe pas).
La lecture dans le fichier se fait par un appel de la fonction fread:
fread (&n, sizeof(int), 1, entree)
dont les arguments sont comparables à ceux de fwrite.
Mais, cette fois, la condition d'arrêt de la boucle est:
feof (entree)
Celle-ci prend la valeur vrai (c'est-à-dire 1) lorsque la fin du fichier
a été rencontrée. Notez bien qu'il n'est pas suffisant d'avoir lu le
dernier octet du fichier pour que cette condition prenne la valeur vrai.
Il est nécessaire d'avoir tenté de lire au-delà;c'est ce qui explique que
nous ayons examiné cette condition après l'appel de fread et non avant.
Remarques:
1) On pourrait remplacer la boucle while par la construction
(moins concise) suivante:
do
{ fread (&n, sizeof(int), 1, entree) ;
if ( !feof(entree) ) printf ("\n%d", n) ;
}
while ( !feof(entree) ) ;
2) N'oubliez pas que le premier argument des fonctions fwrite et fread
est une adresse. Ainsi, lorsque vous aurez affaire à un tableau, il
faudra utiliser simplement son nom (sans le faire précéder de &),
tandis qu'avec une structure il faudra effectivement utiliser l'opérateur
& pour en obtenir l'adresse.
Dans ce dernier cas, même si l'on ne cherche pas à rendre son programme
portable,il sera préférable d'utiliser l'opérateur sizeof pour déterminer
avec certitude la taille des blocs correspondants.
3) fread fournit le nombre de blocs effectivement lus (et non pas le nombre
d'octets lus).Ce résultat peut être inférieur au nombre de blocs demandés
soit lorsque l'on a rencontré une fin de fichier,soit lorsqu'une erreur
de lecture est apparue. Dans notre précédent exemple d'exécution,fread
fournit toujours 1 , sauf la dernière fois où elle fournit 0.

10.3 L'ACCÈS DIRECT
~~~~~~~~~~~~~~~~~~~
Les fonctions fread et fwrite lisent ou écrivent un certain nombre d'octets
dans un fichier, à partir d'une "position courante".
Cette dernière n'est rien d'autre qu'un "pointeur" dans le fichier,
c'est-à-dire un nombre précisant le rang du prochain octet à lire ou à
écrire. Après chaque opération de lecture ou d'écriture, ce pointeur se
trouve incrémenté du nombre d'octets transférés.
C'est ainsi que l'on réalise un accès séquentiel au fichier.
Mais il est également possible d'agir directement sur ce pointeur de
fichier à l 'aide de la fonction fseek. Cela permet ainsi de réaliser
des lectures ou des écritures en n'importe quel point du fichier, sans
avoir besoin de parcourir toutes les informations qui précèdent.
On peut ainsi réaliser ce que l'on nomme généralement un "accès direct".

Le langage C 44

10.3.1 Accès direct en lecture sur un fichier existant
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Voici un prgm qui permet d'accéder à n'importe quel entier d'un fichier
du type de ceux que pouvait créer notre programme du paragraphe 2.1.

#include <stdio.h>
int main(void)
{
char nomfich[21];
int n ;
long num ;
FILE * entree ;
printf ("nom du fichier à consulter : ") ;
scanf ("%20s", nomfich) ;
entree = fopen (nomfich, "r") ;
while(printf(" numéro de l'entier recherché:"),scanf ("%ld",&num),num)
{
fseek (entree, sizeof(int)*(num-1), SEEK_SET)
fread (&n, sizeof(int), 1, entree) ;
printf (" valeur : %d \n", n) ;
}
fclose (entree) ;
return 0 ;
}
La principale nouveauté réside essentiellement dans l'appel de la fonction
fseek:
fseek ( entree, sizeof(int)*(num-1), SEEK_SET);
Cette dernière possède trois arguments:
- le fichier concerné (désigné par le pointeur sur une structure de
type FILE, tel qu'il a été fourni par fopen),
- un entier de type long spécifiant la valeur que l'on souhaite donner au
pointeur de fichier. Il faut noter que l'on dispose de trois manières
d'agir effectivement sur le pointeur, le choix entre les trois étant
fait par l'argument suivant,
- le choix du mode d'action sur le pointeur de fichier: il est défini par
une constante entière. Les valeurs suivantes sont prédéfinies
dans <stdio.h>:
* SEEK_SET (en général 0): le second argument désigne un déplacement
(en octets) depuis le début du fichier.
* SEEK_CUR (en général 1) : le second argument désigne un déplacement
exprimé à partir de la position courante;
il s'agit donc en quelque sorte d'un déplacement relatif dont la
valeur peut, le cas échéant, être négative.
* SEEK_END (en général 2): le second argument désigne un déplacement
depuis la fin du fichier.
Ici, il s'agit de donner au pointeur de fichier une valeur correspondant
à l'emplacement d'un entier (sizeof(int)) octets) dont l'utilisateur
fournit le rang. Il est donc naturel de donner au troisième argument
la valeur 0. Notez, au passage, la "formule" : sizeof(int) * (num-1)
qui se justifie par le fait que nous nous sommes convenu que, pour
l'utilisateur, le premier entier du fichier porterait le rang 1 et non 0.

10.3.2 Les possibilités de l'accès direct
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Outre les possibilités de "consultation immédiate" qu'il procure,
l'accès direct facilite et accélère les opérations de mise à jour d'un
fichier. Mais, de surcroît, l'accès direct permet de remplir un fichier
de façon quelconque.
Ainsi, nous pourrions constituer notre fichier d'entiers en laissant
l'utilisateur les fournir dans l'ordre de son choix,
comme dans cet exemple de programme:
#include <stdio.h>
int main(void)
{
char nomfich[21] ;
FILE * sortie ;
long num ;
int n ;
printf ("nom fichier : ") ;
scanf ("%20s",nomfich) ;
Le langage C 45
sortie = fopen (nomfich, "w") ;
while (printf("\nrang de l'entier : "), scanf("%ld",&num), num)
{
printf ("valeur de l'entier : ") ;
scanf ("%d", &n) ;
fseek (sortie, sizeof(int)*(num-1), SEEK_SET);
fwrite (&n, sizeof(int), 1, sortie) ;
}
fclose(sortie) ;
return 0 ;
}

Or il faut savoir qu'avec beaucoup de systèmes, dès que vous écrivez
le énième octet d'un fichier, il y a automatiquement réservation de la
place de tous les octets précédents; leur contenu, par contre, doit être
considéré comme étant aléatoire.
Dans ces conditions, vous voyez que, à partir du moment où rien n'impose
à l'utilisateur de ne pas "laisser de trous" lors de la création du
fichier,il faudra être en mesure de repérer ces trous lors d'éventuelles
consultations ultérieures du fichier.
Plusieurs techniques existent à cet effet:
- on peut, par exemple, avant d'exécuter le prgm précédent,commencer par
''initialiser" tous les emplacements du fichier à une valeur spéciale,
dont on sait qu'elle ne pourra pas apparaître comme valeur effective,
- on peut aussi "gérer une table des emplacements inexistants", cette table
devant alors être conservée (de préférence) dans le fichier lui-même.
D'autre part, il faut bien voir que l'accès direct n'a d'intérêt que lors-
que l'on est en mesure de fournir le rang de l'emplacement concerné.
Ce n'est pas toujours possible. Ainsi, si l'on considère ne serait ce
qu'un simple fichier de type répertoire téléphonique, on voit qu'en
général on cherchera à accéder à une personne par son nom plutôt que par
son numéro d'ordre dans le fichier.
Cette contrainte qui semble imposer une recherche séquentielle peut
être contournée par la création de ce que l'on nomme un "index",
c'est-à-dire une table de correspondance entre un nom d'individu et sa
position dans le fichier.
Nous n'en dirons pas plus sur ces méthodes spécifiques de gestion de
fichiers qui sortent du cadre de ce polycopié.

10.3.3 En cas d'erreur
~~~~~~~~~~~~~~~~~~~~~~
a) Erreur de pointage
Il faut bien voir que le positionnement dans le fichier se fait sur un
octet de rang donné, et non, comme on pourrait le préférer, sur un "bloc"
(ou "enregistrement") de rang donné. D'ailleurs, n'oubliez pas qu'en
général cette notion d'enregistrement n'est pas exprimée de manière
intrinsèque au sein du fichier. Ainsi, dans notre programme précédent,
vous pourriez, par mégarde, utiliser la formule suivante:
sizeof(int) * num -1
laquelle vous positionnerait systématiquement "à cheval" entre le
dernier octet d'un entier et le premier du suivant.
Bien entendu, les résultats obtenus seraient quelque peu fantaisistes.
Cette remarque prend encore plus d'acuité lorsque vous créez un fichier
à partir de structures. Dans ce cas, nous ne saurions trop vous conseiller
d'avoir systématiquement recours à l'opérateur sizeof pour déterminer la
taille réelle de ces structures.
b) Tentative de positionnement hors fichier
Lorsque l'on accède ainsi directement à l'information d'un fichier,
le risque existe de tenter de se positionner... en dehors du fichier.
En principe, la fonction fseek fournit:
- la valeur 0 lorsque le positionnement s'est déroulé correctement,
- une valeur quelconque dans le cas contraire.

Le langage C 46

Toutefois,beaucoup d'implémentation ne respectent pas la norme à ce sujet.
Dans ces conditions, il nous parait plus raisonnable de programmer une
protection efficace en déterminant, en début de programme, la taille
effective du fichier à consulter.
Pour cela, il suffit de vous positionner en fin de fichier avec fseek puis
de faire appel à la fonction ftell qui vous restitue la position courante
du pointeur de fichier. Ainsi, dans notre précédent programme, nous pour-
rions introduire les instructions:
long taille;
.....
fseek ( entree, 0, SEEK_END );
taille = ftell (entree);

Il suffit alors de vérifier que la position de l'enregistrement demandé
(ici: sizeof(int)*(num-1)) est bien inférieure à la valeur de taille pour
éviter tout problème.

10.4 LES ENTRÉES-SORTIES FORMATÉES ET LES FICHIERS DE TEXTE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nous venons de voir que les fonctions fread et fwrite réalisent un trans-
fert d'information (entre mémoire et fichier) que l'on pourrait qualifier
de "brut", dans le sens où il se fait sans aucune transformation de
l'information.
Les octets qui figurent dans le fichier sont des "copies conformes" de
ceux qui apparaissent en mémoire.
Mais, en langage C, il est également possible d'accompagner ces transferts
d'information d'opérations de "formatage" analogues à celles que réalisent
printf ou scanf.
Les fichiers concernés par ces opérations de formatage sont alors ce que
l'on a coutume d'appeler des "fichiers de type texte" ou encore
des "fichiers de texte". Ce sont des fichiers que vous pouvez manipuler
avec un éditeur quelconque, un traitement de texte quelconque ou, plus
simplement, lister par les commandes appropriées du système d'exploitation
(TYPE ou PRINT sous DOS).
Dans de tels fichiers, chaque octet représente un caractère.
Généralement, on y trouve des caractères de fin de ligne (\n), de sorte
qu'ils apparaissent comme une suite de lignes.
Les fonctions permettant de travailler avec des fichiers de texte ne sont
rien d'autre qu'une généralisation aux fichiers de celles que nous avons
déjà rencontrées pour les entrées-sorties conversationnelles.
Nous nous contenterons donc d'en fournir une brève liste:
- fscanf ( fichier, format, liste_d'adresses )
- fprintf ( fichier, format, liste_d'expressions )
- fgetc ( fichier )
- fputc (entier, fichier )
- fgets ( chaîne, lgmax, fichier )
- fputs ( chaîne, fichier )
La signification de leurs arguments est la même que pour les fonctions
conversationnelles correspondantes. Seule fgets comporte un argument
entier (lgmax) de contrôle de longueur.
Il précise le nombre maximal de caractères (y compris le \0 de fin)
qui seront placés dans la chaîne.
Leur valeur de retour est la même que pour les fonctions conversationnlles.
Cependant, il nous faut apporter quelques indications supplémentaires qui
ne se justifiaient pas pour des entrées-sorties conversationnelles, à
savoir que la valeur de retour fournie par fgetc est du
type int (et non, comme on pourrait le croire, de type char).
Lorsque la fin de fichier est atteinte, cette fonction fournit la
valeur EOF (constante prédéfinie dans <stdio.h> - en général -1).
La fin de fichier n'est détectée que lorsque l'on cherche à lire un
caractère alors qu'il n'y en a plus de disponible, et non pas, dès que
l'on a lu le dernier caractère.
D'autre part, notez bien que cette convention fait, en quelque sorte,
double emploi avec la fonction feof.

Le langage C 47

D'une manière générale, toutes les fonctions présentées ci-dessus fournis-
sent une valeur de retour bien définie en cas de fin de fichier ou
d'erreur.
Remarque importante:
A priori, on peut toujours dire que n'importe quel fichier, quelle que
soit la manière dont l'information y a été représentée, peut être
considéré comme une suite de caractères. Bien entendu, si l'on cherche
à "lister", par exemple, le contenu d'un fichier tel que celui créé dans
le paragraphe 2.1 (suite d'entiers), le résultat risque d'être sans
signification (on obtiendra une suite de caractères apparemment
quelconques, sans rapport aucun avec les nombres enregistrés).
Mais, sans aller jusqu'à le lister, on peut se demander s'il ne serait
pas possible de le recopier, à l'aide d'une répétition de fgetc et de
fputc.
Or cela semble effectivemnt possible puisque ces fonctions se contentent
de prélever un caractère (donc un octet) et de le recopier tel quel.
Ainsi, quel que soit le contenu de l'octet lu, on le retrouvera dans le
fichier de sortie.
En réalité, cela n'est que partiellement vrai car certains systèmes
(DOS en particulier) distinguent les fichiers de texte des autres
(qu'ils appellent parfois "fichiers binaires"); plus précisément, lors de
l'ouverture du fichier,on peut spécifier ou non considérer le contenu
du fichier comme du texte. Cette distinction est en fait motivée par le
fait que le caractère de fin de ligne (\n) possède, sur ces systèmes,
une représentation particulière obtenue par la succession de deux
caractères (retour chariot \r, suivi de fin de ligne \n).
Or, dans ce cas, pour qu'un programme C puisse ne "voir" qu'un seul
caractère de fin de ligne et qu'il s'agisse bien de \n, il faut opérer
un traitement particulier consistant à :
- remplacer chaque occurrence de ce couple de caractères par \n, dans le
cas d'une lecture,
- remplacer chaque demande d'écriture de \n par l'écriture de ce couple de
caractères.
Bien entendu, de telles substitutions ne doivent pas être réalisées sur
de "vrais fichiers binaires".
Il faut donc bien pouvoir opérer une distinction au sein du programme.
Cette distinction se fait au moment de l'ouverture du fichier, en ajoutant
une lettre t (pour "texte") ou b (pour "binaire") au mode d'ouverture.
En général, dans les implémentations où l'on distingue les fichiers de
texte des autres, les fonctions d'entrées-sorties formatées refusent de
travailler avec un fichier qui n'a pas été spécifié de ce type lors
de son ouverture.

10.5 LES DIFFÉRENTES POSSIBILITÉS D'OUVERTURE D'UN FICHIER
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Dans nos précédents exemples, nous n'avons utilisé que les modes w et r.
Nous vous fournissons ici la liste des différentes possibilités offertes
par fopen.
r : lecture seulement; le fichier doit exister.
w : écriture seulement. Si le fichier n'existe pas, il est créé.
S'il existe, son (ancien) contenu est perdu.
a : écriture enfin de fichier (append). Si le fichier existe déjà,
il sera "étendu". S'il n'existe pas, il sera créé - on se
ramène alors au mode w.
r+: mise à jour (lecture et écriture). Le fichier doit exister.
Notez qu'alors il n'est pas possible de réaliser une lecture à la
suite d'une écriture ou une écriture à la suite d'une lecture,
sans positionner le pointeur de fichier par fseek.
Il est toutefois possible d'enchaîner plusieurs lectures ou écritures
consécutives (de façon séquentielle).
w+: création pour mise à jour.Si le fichier existe,son (ancien) contenu
sera détruit. S'il n'existe pas, il sera créé.
Notez que l'on obtiendrait un mode comparable à w+ en ouvrant un fichier
vide (mais existant) en mode r+.
a+: extension et mise à jour. Si le fichier n'existe pas, il sera créé.
S'il existe, le pointeur sera positionné en fin de fichier.

Le langage C 48

t ou b: lorsque l'implémentation distingue les fichiers de texte des
autres, il est possible d'ajouter l'une de ces deux lettres à chacun
des 6 modes précédents.
La lettre t précise que l'on a affaire à un fichier de texte;
la lettre b précise que l'on a affaire à un fichier ''binaire" .

10.6 LES FICHIERS PRÉDÉFINIS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Un certain nombre de fichiers sont connus du langage C, sans qu'il soit
nécessaire ni de les ouvrir ni de les fermer.

stdin : unité d'entrée (par défaut, le clavier)
stdout: unité de sortie (par défaut, l'écran)
stderr: unité d'affichage des messages d'erreurs (par défaut, l'écran)

On trouve parfois également:

stdaux: unité auxiliaire
stdprt: imprimante

Les deux premiers fichiers correspondent aux unités standard d'entrée et
de sortie d'un programme. Lorsque vous exécutez un programme depuis
le système, vous pouvez éventuellement "rediriger" ces fichiers.
Par exemple, la commande système suivante (valable à la fois sous
UNIX et sous DOS)

TRUC < DONNEES > RESULTATS

exécute le programme TRUC, en utilisant comme unité d'entrée le
fichier DONNEES et comme unité de sortie le fichier RESULTATS.
Dans ces conditions, une instruction telle que, par exemple, fgetchar
deviendrait équivalente à fgetc(fich) où fich serait un flux obtenu
par appel à fopen.
De même, scanf(...) deviendrait équivalent à fscanf(fich, . .) , etc.
Notez bien qu'au sein du programme même il n'est pas possible de savoir
si un fichier prédéfini a été redirigé au moment du lancement du prgrm;
autrement dit, lorsqu'une fonction comme fgetchar ou scanf lit des
informations, elle ne peut absolument pas savoir si ces dernières
proviennent du clavier ou d'un fichier.
Dans le paragraphe 2.3 du chapitre consacré aux chaînes de caractères,
nous vous avons montré comment régler les problèmes posés par scanf,
en faisant appel à l'association des deux fonctions gets et sscanf.
Pour ce faire, nous avions dû toutefois supposer que les "lignes'' lues
par gets ne dépasseraient pas une certaine longueur. Cette hypothèse est
déjà restrictive dans le cas d'informations provenant du clavier: même
si cela peut paraître naturel à la plupart des utilisateurs de ne pas
dépasser, par exemple, une largeur d'écran, le risque existe d'en voir
certains entrer une ligne trop longue qui "plantera" le programme.
Cette même hypothèse devient franchement intolérable dans le cas de
lecture dans un fichier (sur lequel peut avoir été redirigée l'entrée
standard !).

En fait, il est très simple de régler définitivement ce problème.
Il suffit d'employer,à la place de (revoyez l'exemple du paragraphe 2.3
du chapitre consacré aux chaînes): gets (ligne);
une instruction telle que (LG désignant le nombre maximal de caractères
acceptés): fgets (ligne, LG, stdin)

10.7 Exercices
~~~~~~~~~~~~~~
1) Ecrire un programme permettant d'afficher le contenu d'un fichier texte
en numérotant les lignes. Les lignes seront supposées ne jamais compor-
ter plus de 80 caractères.

Le langage C 49

2) Ecrire un programme permettant de créer séquentiellement un fichier
"répertoire" comportant pour chaque personne:
- nom (20 caractères maximum),
- prénom (15 caractères maximum),
- âge (entier),
- numéro de téléphone (11 caractères maximum).
Les infos relatives aux différentes personnes seront lues au clavier.

3) Ecrire un programme permettant, à partir du fichier créé par l'exercice
précédent, de retrouver les informations correspondant à une personne
de nom donné.

4) Ecrire un programme permettant, à partir du fichier créé par le prgm de
l'exercice 2, de retrouver les informations relatives à une personne de
"rang" donné (par accès direct).

5) Ecrire un programme permettent de trier par ordre alphabétique du nom,
les informations du fichier crée à l’exercice 2.
6) Ecrire un programme regroupant tous ces « sous programme » avec
possibilité de menu de choix pour l’utilisateur.
Pour tous ces exercices utiliser une structure.

Le langage C 50


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Le_C_fr\11. LA GESTION DYNAMIQUE DE LA MEMOIRE.rtf

11. LA GESTION DYNAMIQUE DE LA MEMOIRE
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

11.1 LES OUTILS DE BASE DE LA GESTION DYNAMIQUE: MALLOC ET FREE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Commençons par étudier les deux fonctions les plus classiques de gestion
dynamique de la mémoire, à savoir malloc et free.

1.1.1 La fonction malloc
~~~~~~~~~~~~~~~~~~~~~~~~
a) Premier exemple
Considérez ces intructions:
#include <stdlib.h>
.....
char * adr;
.....
adr = malloc (50),
.....
for (i=0; i<50; i++) *(adr+i) = 'x';
L'appel : malloc (50) alloue un emplacement de 50 octets dans le
‘’tas’’ et en fournit l'adresse en retour. Ici, cette dernière est
placée dans le pointeur adr.
L'instruction for qui vient à la suite n'est donnée qu'à titre d'exemple
d'utilisation de la zone ainsi créée.

b) Second exemple
long * adr;
.....
adr = malloc (100 * sizeof(long));
for (i=0; i<100; i++) *(adr+i) = 1;
Cette fois, nous nous sommes alloué une zone de 100 * sizeof(long) octets
(notez l'emploi de sizeof qui assure la portabilité).
Mais nous l'avons considérée comme une suite de 100 entiers de type long.
Pour ce faire, vous voyez que nous avons pris soin de placer le résultat
de malloc dans un pointeur sur des éléments de type long.
N'oubliez pas que les règles de l'arithmétique des pointeurs font que:
adr + i
correspond à l'adresse contenue dans adr,
augmentée de sizeof(long) fois la valeur de i (puisque adr pointe sur
des objets de longueur sizeof(long)).

c) D'une manière générale
Le prototype de malloc (qui figure à la fois dans stdlib.h et dans alloc.h)
est précisément:
void * malloc (size t taille) (sldlib.h) (alloc.h)
Il montre tout d'abord que le résultat fourni par malloc est un "pointeur
générique" (revoyez éventuellement le paragraphe correspondant du chapitre
relatif aux pointeurs). Il pourra donc être converti implicitement en un
pointeur de n' importe quel type; ainsi, dans nos précédents exemples,
il a pu être converti en char * ou en long *.
Une telle conversion peut apparaître relativement fictive,dans la mesure
où l'adresse correspondante n'est pas modifiée par la conversion.
Elle n'en a pas moins une grande importance puisque,comme nous l'avons
déjà mentionné à diverses reprises, la connaissance du type d'un pointeur
intervient dans les calculs arithmétiques portant sur ce pointeur.
D'autre part, nous constatons que l'unique argument de malloc est d'un type
a priori inattendu (nous aurions pu penser à int ou long).
En fait, size_t est un nom de type prédéfini (par typedef).
Le type exact lui correspondant ...

Le langage C 51

... dépend de l'implémentation. Cela signifie donc que la taille maximale
des objets que l'on peut s'allouer par malloc dépend de l'implémentation.
Enfin, il faut savoir que malloc fournit un pointeur nul (NULL) dans le cas
où l'allocation mémoire a échoué.
Bien entendu, dans un programme "opérationnel", il sera nécessaire de
s'assurer qu'aucun problème de cette sorte n'apparaît.

11.1.2 La fonction free
~~~~~~~~~~~~~~~~~~~~~~~~
L'un des intérêts essentiels de la gestion dynamique est de pouvoir
récupérer des emplacements dont on n'a plus besoin. Le rôle de la
fonction free est de libérer un emplacemnt préalablement alloué.
Voici un exemple de programme, exécuté ici dans un environnement DOS.
Il vous montre comment malloc peut profiter d'un espace préalablemnt
libéré sur le tas.

#include <stdio.h>
#include <alloc.h>
int main(void)
{
char * adr1, * adr2, * adr3 ;
adr1 = malloc (100) ;
printf ("allocation de 100 octets en %p\n", adr1) ;
adr2 = malloc (50) ;
printf ("allocation de 50 octets en %p\n", adr2) ;
free (adr1) ;
printf ("libération de 100 octets en %p\n", adr1) ;
adr3 = malloc (40) ;
printf ("allocation de 40 octets en %p\n", adr3) ;
return 0 ;
}
allocation de 100 octets en 06AC
allocation de 50 octets en 0714
libération de 100 octets en 06AC
allocation de 40 octets en 06E8

Vous notez que la dernière allocation a pu se faire dans l'espace libéré
par le précédent appel de free.
Remarques:
1) Dans cet exemple, vous pouvez constater que l'allocation d'une zone
de 100 octets nécessite en fait un peu plus de place mémoire
(exactement 104 octets). La différence (4 octets) correspond à
des "octets de service" dans lesquels le système place les informations
nécessaires à sa gestion dynamique de la mémoire.
2) La norme prévoit que malloc alloue convenablement de l'espace,en tenant
compte d'éventuelles contraintes d'alignement de l'objet concerné.
Or, en toute rigueur, malloc n'a pas d'information précise sur le type
de l'objet (elle n'en a que la longueur !). Dans ces conditions, le
respect de la norme peut prendre des allures différentes
suivant l'implémentation :
- alignement basé sur la taille demandée,
- alignement systématique tenant compte de la contrainte d'alignement la plus forte.
Dans tous les cas, bien sûr, aucun risque n'existe. Simplement, la taille
mémoire réellement utilisée pourra, pour un type donné, différer d'une
implémention à une autre (notez qu'en toute rigueur c'est déjà ce qui se
produit avec les "octets de service" dont le nombre peut varier avec
l'implémentation).

11.2 Exercice :
~~~~~~~~~~~~~~
Utiliser ces nouveautés pour compléter votre programme sur les fichiers.

Le langage C 52


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Entete_Niveau_1.rtf

INITIATION AU LANGAGE C

BERTHOMIER ERIC

APPRENTISSAGE DE LA SYNTAXE DE BASE

Version 1.1 du 13 Mars 2000


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_1.rtf

Premiers Pas

1.1 Prologue

Ce cours utilise comme compilateur le Borland C 2.0. Celui-ci est disponible sur le Web.

1.2 Exemple de programme
main ()
{
puts ("Bonjour");
}

Voici pour exemple un premier programme qui fonctionne malgré le fait qu’il ne soit pas normalisé. Celui-ci affiche le mot bonjour à l’écran.
Une fois le texte du programme frappé, il faut le compiler (Compile), c’est à dire en analyser la syntaxe.

On remarquera plusieurs types de commandes dans le menu Compile :
Compile qui compile le programme c’est à dire en analyse la syntaxe et
produit un pseudo-code non interprétable par l’ordinateur (.obj)
Make qui construit un exécutable .exe qui permet au programme d'être
exécuter comme n'importe quel autre programme.
Link qui permet de lier plusieurs pseudo-codes entre eux et d'en créer un
Build all qui permet de retraduire tous les codes source d’un projet (ensemble
de programmes liés les uns aux autres).
Code source : le code source représente le programme sous sa forme de langage C,
c'est à dire ce que vous tapez dans l'éditeur de texte du compilateur.
Pour notre part, nous n’utiliserons que Compile, le reste se faisant durant la exe lors de l’exécution de la commande Run.

BERTHOMIER Eric Page 1/524/03/00
Initiation au langage C.

1.3 Exécution du programme (Run)
On remarque que :
Pour voir ce qu’affiche le programme il est nécessaire d’utiliser la commande
UserScreen dans le menu Windows.
L’exécution du programme de nombreuses fois fait apparaître le mot Bonjour
plusieurs fois.
1.4 Correction du programme

Nous allons normaliser le programme. En fait, à sa base, le langage C n’est qu’un ensemble de bibliothèques à partir desquelles le compilateur trouve les fonctions et les applications qui lui permettent de créer un programme exécutable. Exactement ce que vous faites lorsque vous recherchez dans une encyclopédie.

Certaines bibliothèques sont inclues dans des compilateurs ce qui permet à notre
programme de s’exécuter. Normalement, puts a besoin de la bibliothèque stdio.h. Pour
ajouter une bibliothèque, il suffit d’ajouter #include <nom de la bibliothèque> en début de
programme.
Le second point à corriger est l’absence de valeur de retour. La valeur de retour permet

un programme ou à l’utilisateur de savoir si le programme que l'on exécute s’est correctement terminé. 0 signifie une terminaison sans erreur.

En lui rajoutant quelques fonctionnalités on obtient donc :

#include <stdio.h> ibliothèques
#include <conio.h>
int main ()
{
clrscr (); /* Efface l’écran */
puts ("Bonjour"); Corps du programme
getch (); /* Attendre */
return (0);
}

La valeur de retour n'est pas obligatoire, pour ne pas utiliser de valeur de retour on utilise
void main () à la place de int main (). void peut se traduire par "ne contenant rien".
Attention: dans ce cas, on utilise return; et non return (0).
Le programme devient donc :
#include <stdio.h>
#include <conio.h>
void main ()
{
clrscr (); /* Efface l’écran */
puts ("Bonjour");
getch (); /* Attendre */
return; /* Facultatif car c'est la dernière ligne (fin)*/
}

BERTHOMIER Eric Page 2/524/03/00
Initiation au langage C.

1.5 Petit mot sur ce qu'est une bibliothèque

A l'instar de l'étudiant qui recherche dans des livres, on peut dire que le ".h" représente l'index du livre et le ".c" le contenu du chapitre concerné, le ".o" ou ".

Exemple
#13#10
Lorsque le compilateur C rencontre le mot clrscr, il regarde dans chacun des ".h" déclaré par l'instruction #include si ce mot y est défini. Il trouve celui-ci dans la conio.h et remplace donc ce mot par le code qui lui est associé au moment

de la compilation. A l'inverse, s'il ne le trouve pas, celui-ci émettra une erreur de syntaxe.

1.6 Un exemple de fichier bibliothèque
Vous trouverez ci-dessous, un extrait de la bibliothèque stdio.h. On y retrouve
notamment la définition de puts que l'on voit dans ce cours et la définition de printf
que l'on verra dans le 2nd cours.
Extrait du fichier stdio.h

/* stdio.h
Definitions for stream input/output.
Copyright (c) Borland International 1987,1988
All Rights Reserved.
*/
#if !defined(__STDIO_DEF_)
#define __STDIO_DEF_
int _Cdecl printf (const char *format, ...);
int _Cdecl puts (const char *s);
#endif

1.7 Les différentes fonctions

puts : permet d’afficher du texte.
clrscr : permet d’effacer l’écran.
getch : permet d’attendre la frappe d’une touche.
/* Commentaire*/ : permet de mettre un commentaire.

Notre programme efface l’écran puis affiche bonjour et attend que l’on appuie sur une touche afin que l’on puisse voir ce qu’il a écrit.

1.8 Squelette de programme
On peut définir le squelette d’un programme C de la façon suivante : /* Déclaration des bibliothèques */
int main ()
{

BERTHOMIER Eric Page 3/524/03/00
Initiation au langage C.

/* Déclaration des variables */ cf. chapitre 2
/* Corps du programme */
getch () ; /* Facultatif mais permet de voir ce qui s’est produit à l’écran */
/* En attendant l'appui d'une touche */
return (0) ; /* Aucune erreur renvoyée */
}
1.9 Les blocs

La partie de programme située entre deux accolades est appelée un bloc. Je conseille de prendre l’habitude de faire une tabulation après le retour à la ligne qui suit l’accolade. Puis retirer cette tabulation après l’accolade fermante du bloc. Ainsi, on obtient :

{
Tabulation Bloc de programme
Tout le code est frappé à cette hauteur
}
Retrait de la tabulation
Tout le texte est maintenant frappé à cette hauteur.

Cette méthode permet de contrôler la fermeture de toutes les accolades et leurs correspondances.

1.10 Les commentaires

Commenter signifie qu’une personne ne connaissant pas le langage C doit pouvoir lire le programme et le comprendre.

Les commentaires sont indispensables dans tout bon programme.
Les commentaires peuvent être placés à n’importe quel endroit dans le programme. Ils
commencent par /* et se termine par */.
/* Commentaire */
1.11 Exercices d’application
Ecrire un programme qui écrit au revoir.
Ecrire un programme qui :
Ecrit « Salut toi, appuie sur une touche s’il te plaît
Attend l’appui d’une touche
Efface l’écran
Ecrit « Merci d’avoir appuyé sur une touche
Commentez le précédent programme.
Exemple :
puts ("Cours de programmation" ) ;
/* Ecrit ‘Cours de programmation’ à l’écran */
Ecrire un programme qui écrit
Hamlet says To be or not to be, that is the question. »

BERTHOMIER Eric Page 4/524/03/00
Initiation au langage C.

Corrigés des exercices du chapitre 1

Ecrire un programme qui écrit au revoir

#include <stdio.h>
#include <conio.h>
main()
{
clrscr(); /* Efface l'écran */
puts("Au revoir "); /* Affiche Au revoir */
getch ();
}
Ecrire un programme qui :

Ecrit « Salut toi, appuie sur une touche s’il te plaît
Attend l’appui d’une touche
Efface l’écran
Ecrit « Merci d’avoir appuyé sur une touche

#include <stdio.h>
#include <conio.h>
int main ()
{
clrscr (); /* Efface l'écran */

puts ("Salut toi, appuie sur une touche s'il te plaît"); /* Affiche le message Salut toi, … s'il te plaît */

getch (); /* Attend la frappe d'une touche */
puts ("Merci d'avoir appuyé sur une touche");
/* Affiche le message Merci d'avoir appuyé sur une touche */
return 0; /* Revient */
}
Commentez le précédent programme.

Ecrire un programme qui :

Ecrit : « Hamlet says To be or not to be, that is the question. »
#include <stdio.h>
#include <conio.h>
int main()
{
clrscr ();

puts ("Hamlet says To be or not to be, that is the question."); getch ();

return 0;
}

BERTHOMIER Eric Page 5/524/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_2.rtf

Les variables (1ère partie)

2.1 Printf : fonction indispensable pour afficher une variable

Exemple :
#include <stdio.h>
int main ()
{

printf ("Coucou c'est moi"); /* Affiche Coucou c'est moi à l'écran */ getch (); /* Attendre l'appui d'une touche */

}
On pourrait dire que la fonction printf est la même que l'instruction puts vu

précédemment mais il n'en est rien … Celle-ci est beaucoup, beaucoup, beaucoup plus puissante.

Syntaxe:

La syntaxe de printf est très complexe et pourrait à elle seule faire l'objet d'un cours, nous en verrons donc des applications au fur et à mesure des besoins.

2.2 Variable

Comme son nom l'indique une variable est quelque chose qui varie. C'est vrai mais ce n'est pas suffisant.
Une variable peut être considérée comme une boîte dans laquelle on met des données que l'on peut lire ou écrire.

Variable : Lire ou Ecrire
In : entrée de données dans la variable.

Boîte [Variable]

Out : lecture de la donnée.
La manière la plus facile de lire le contenu d'une variable est la fonction printf que l'on a
aperçu précédemment.
La manière la plus simple de donner une valeur à une variable est l'opérateur mathématique
. Ecrire dans une variable ayant déjà une valeur revient à la modifier.

Une variable ne peut contenir qu'une seule chose à la fois. Si vous mettez une seconde donnée dans la variable, la précédente est effacée.

2.3 Déclaration d'une variable
La déclaration d'une variable se fait simplement en écrivant :
<son type> <son nom>;

Berthomier Eric Page 1/6 08/06/00
Initiation au langage C.

Exemples de type de variables :
char : caractère
int: entier

2.4 Application : exemple
#include <stdio.h>
int main ()
{
int i; /* i : variable de type entier */ Déclarations des variables
char car; /* car: variable de type caractère */

i = 65; /* i vaut 65 */ Ecriture dans les variables
car = 'E'; /* car vaut E */
clrscr (); /* Efface l'écran */

printf ("i vaut %d.\n", i); /* Affiche la valeur de i */ Affichage des variables
printf ("car vaut %c.\n",car); /* Affiche la valeur de car */
getch (); /* Attendre l'appui d'une touche */
return (0);
}
Explications :
On met dans la variable i la valeur 65.
On met dans la variable car la valeur de E.
Note : En informatique, tout n'est que nombre, je dis donc la valeur de E et non E car c'est

le code Ascii de E qui est sauvegardé dans cette variable. Nous reviendrons là dessus un peu plus tard.

printf ("i vaut %d.\n", i);
%d signifie que l'on attend une valeur décimale , le programme va donc remplacer le
%d par la valeur de i.
printf ("car vaut %c.\n", car);
%c signifie que l'on attend une valeur de type caractère , le programme va donc
remplacer le %c par la valeur de car.
\n permet de réaliser un retour chariot c'est à dire un retour à la ligne.
2.5 Utilisation multiple du %
Le code "% x" signifie que le compilateur C doit remplacer ce code par la valeur
correspondante (qui lui est fourni dans la suite de l'instruction) en la transformant dans le
type x. Cette transformation est appelé un cast.

Berthomier Eric Page 2/6 08/06/00
Initiation au langage C.

Exemple :
int i;
i =65;
printf ("Le caractère %d est %c",i,i);
nous donnera l'affichage suivant :
Le caractère 65 est A.
Le %d est remplacé par la valeur numérique de i c'est à dire 65.
Le %c est remplacé par la valeur alphanumérique (ASCII) de i c'est à dire A.
cf. Table Ascii en Annexe.
2.6 Exercices d'applications directes

En utilisant ce qui a été fait précédemment, faîtes afficher les valeurs 70, 82, 185 et 30. En utilisant ce qui a été fait précédemment, faîtes afficher, les caractères c, o, u, C, O, U.

2.7 Réutilisation d'une variable

On peut réutiliser une variable autant de fois que l'on veut, la précédente valeur étant effacée.
i = 3; i = 5; i = 7; donnera au final pour valeur de i la valeur de 7. car = 'E'; car = 'G'; car = 'h'; donnera au final pour valeur de car la valeur de 'h'.

2.8 Caractères spéciaux

Certains caractères utilisés par la fonction printf ("%" par exemple) ou même tout simplement pour déclarer une variable (' pour les caractères par exemple) oblige à utiliser le

caractère de suffixe \ pour pouvoir être affiché.
Exemple :
Pour afficher un % avec printf j'écrirai :
printf "La réduction était de 20 \%"
Pour déclarer un caractère avec la valeur ' (prononcée cote en informatique et non pas
apostrophe (français)), on écrira :
char car;
car = '\ '';
2.9 Exercices à réaliser
Faire un programme qui réalise l'affichage suivant en utilisant les variables
Aide : Sans retour chariot, on affiche à la suite
Coucou
17
De la même façon, réaliser un programme qui réalise l'affichage suivant : C

O U

Berthomier Eric Page 3/6 08/06/00
Initiation au langage C.

Cou
1

2 3

456
C'est rigolo …
Rappel : pour mettre une variable c égale à ' on écrit c = '\''
Ecrire un programme qui écrit
Hamlet says "To be or not to be, that is the question." »
avec les " bien sûr.
Rappel : Pour pouvoir afficher un caractère de syntaxe C, par exemple ", on utilise
le caractère \ comme préfixe à ce caractère. Pour obtenir un ", on utilise donc \ ".

Berthomier Eric Page 4/6 08/06/00
Initiation au langage C.

Corrigés des exercices du chapitre 2

Faites afficher, en utilisant ce qui a été fait précédemment, les valeurs 70, 82,
185 et 30.
#include <stdio.h> clrscr ();
printf ("i vaut %d.\n",i);
int main () printf ("a vaut %d.\n",a);
printf ("b vaut %d.\n",b);
int i,a,b,c; printf ("c vaut %d.\n",c);
i=70; getch ();
a=82;
b=185; return 0;
c=30; }

Faites afficher, en utilisant ce qui a été fait précédemment, les caractères c,
u, C, O, U.

#include <stdio.h> printf ("a vaut %c.\n",a);
printf ("b vaut %c.\n",b);
int main () printf ("c vaut %c.\n",c);
printf ("d vaut %c.\n",d);
char a,b,c,d,e,f; printf ("e vaut %c.\n",e);
a='c'; printf ("f vaut %c.\n",f);
b='o';
c='u'; printf ("a, b, c, d, e, f
d='C'; valent : %c, %c, %c, %c, %c,
e='O'; %c.\n",a,b,c,d,e,f);
f='U';
clrscr (); getch ();
return (0);
}

Faire un programme qui réalise l'affichage …

Coucou
17

#include <stdio.h> car = 'u';
printf ("%c",car);
int main ()
printf ("\n%d",nbre); char car = 'C';
int nbre = 17; /* Attente */
getch ();
printf ("%c",car);
car = 'o'; return (0);
printf ("%c",car); }
car = 'u';
printf ("%c",car);
car = 'c';
printf ("%c",car);
car = 'o';
printf ("%c",car);

Berthomier Eric Page 5/6 08/06/00
Initiation au langage C.

Faire un programme qui réalise l'affichage …

C

O U …

#include <stdio.h>
car = 'C';
int main () printf ("\n%c",car);
car = '\'';
char car = 'C'; printf ("%c",car);
int nbre = 1; car = 'e';
printf ("%c",car);
clrscr (); /* Efface l'écran car = 's';
*/ printf ("%c",car);
car = 'C'; car = 't';
printf ("\n%c",car); printf ("%c",car);
car = 'O'; car = ' ';
printf ("\n%c",car); printf ("%c",car);
car = 'U'; car = 'r';
printf ("\n%c",car); printf ("%c",car);
car = 'C'; car = 'i';
printf ("\n%c",car); printf ("%c",car);
car = 'o'; car = 'g';
printf ("%c",car); printf ("%c",car);
car = 'u'; car = 'o';
printf ("%c",car); printf ("%c",car);
car = 'l';
printf ("\n%d",nbre); printf ("%c",car);
nbre = 2; car = 'o';
printf ("\n%d",nbre); printf ("%c",car);
nbre = 3; /* Attente */
printf ("\n%d",nbre); getch ();
nbre = 456;
printf ("\n%d",nbre); return (0);
}
Faire un programme qui écrit "Hamlet says … "
#include <stdio.h>
int main ()
{
clrscr (); /* Efface l'écran */

printf ("Hamlet says : \"To be or no to be, that is the question.\"");

/* Attente */
getch ();
return (0);
}

Berthomier Eric Page 6/6 08/06/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_3.rtf

Les variables (2nde partie)

Durant tout ce chapitre, nous aurons pour but simple, mais complet et complexe dans sa programmation, de faire une malheureuse calculatrice.

3.1 Exercice de mise en bouche
Ecrire un programme qui :
Ecrit "Calculatrice :" et saute 2 lignes.
Ecrit "Valeur de a :" et saute 1 ligne.
Attend l'appui d'une touche.
Ecrit "Valeur de b :" et saute 1 ligne.
Attend l'appui d'une touche
Ecrit "Valeur de a + b :"
Pas à pas, nous allons maintenant réaliser notre petit programme de calculatrice.
3.2 Déclaration des variables
Complétez le programme en :
Déclarant 2 variables a et b de type int (entier).
Assignant à ces deux variables les valeurs de 36 et 54.
Faisant afficher le résultat de la somme de a + b (et non pas afficher 90 !).

Pour faire afficher le résultat il est possible d'utiliser la fonction printf directement ou indirectement en utilisant une troisième variable. Pour plus de facilité, nous allons utiliser un affichage direct. Celui-ci s'effectue de la façon suivante :

N.B. printf ("Valeur de a + b : %d",a+b);
Le %d est remplacé par la valeur de a+b
3.3 Saisie des variables

Si une calculatrice se limitait à exécuter la somme de deux nombres fixes, le boulier serait encore de mise …

Pour saisir une variable, il est nécessaire d'utiliser la fonction scanf. La fonction scanf s'utilise de la façon suivante :
Saisie de la valeur a : scanf ("%d", &a);
Comme pour printf, on reconnaît le %d pour la saisie d'un nombre décimal. Le & devant le a signifie que l'on va écrire dans la variable a.

Aïe ! En fait &a signifie l'adresse mémoire de a. Scanf va donc écrire
dans l'emplacement mémoire (la petite boîte contenant la
variable) de a. Si l'on oublie le &, on écrira chez quelqu'un d'autre, à une autre adresse. Et là ça peut faire mal, très mal … Mais ceci est une autre histoire …

Nous allons maintenant saisir les variables a et b. Pour exemple, je mets ici le code pour la saisie de a, la saisie de b reste à faire par vos soins à titre d'exercice.

BERTHOMIER Eric Page 1/624/03/00
Initiation au langage C.

/* Saisie de la valeur de a */
printf ("Valeur de a :\n");
scanf ("%d", &a);

Il est conseillé d'initialiser les variables avant de les utiliser, de ce fait, au début du programme, après leur déclaration, assignez à a et à b la valeur de 0.

Exemple :
a = 0;
Si tout c'est bien passé, vous avez maintenant entre les mains une super calculatrice

N.B. Appuyez sur la touche Enter après avoir taper votre valeur.
3.4 Déclaration avec initialisation d'une variable

Pour aller plus rapidement, il est possible d'initialiser une variable dans le même temps que sa déclaration. Pour cela, rien de plus simple, ajouter à la fin de la déclaration = suivi de la valeur d'initialisation.

Exemple :
int i = 10;
3.5 Evolution du logiciel : exercice

Aïe Aïe Aïe Dur Dur d'être informaticien. J'ai envie de faire une super calculatrice et je me dis qu'en fait la simplicité qui a eu lieu tout à l'heure n'est pas forcément adapté à

Assigner une troisième valeur de type int (penser à l'initialiser à 0) que l'on nommera
bêtement s comme somme.
Une fois les valeurs de a et b saisies, initialisez s avec la valeur de a + b. Comme en
mathématiques élémentaires.
Affichez la valeur de s. On devrait avoir les même résultats qu'auparavant, off course.
3.6 Exercices d'application
Réalisez 2 petits programmes qui font :
la soustraction de 2 nombres
la multiplication de 2 nombres
3.7 Un nouveau type: les nombres à virgule ou flottants (float)
Le type float permet de déclarer un nombre à virgule. Transformez les 3 précédents

programmes en utilisant le type float au lieu du type int et %f pour saisir les valeurs de a et b, et pour afficher le résultat.

Petite aide :
float a;
printf ("Saisie de a :");
scanf ("%f",&a);
printf ("\na vaut : %f",a);
getch ();

BERTHOMIER Eric Page 2/624/03/00
Initiation au langage C.

Ce petit morceau de programme saisit la valeur de a et la fait afficher. Pour un affichage plus agréable il est possible de fixer le nombre de chiffres après la

virgule de la façon suivante %.[nombre de chiffres après la virgule]f .
Exemple :
printf ("%.2f",a);
3.8 Ouah, les 4 opérations !!!

Créer un quatrième programme qui réalise la division de deux nombres. Tester avec une division par 0 !!!

La solution à ce petit problème sera vu dans le cours des conditions (if).
3.9 Exercices à réaliser
Compléter les 4 programmes afin de réaliser les opérations suivantes :
s = a + b + c

s = a – b – c s = a / b / c

s = a* b * c
a,b,c sont des nombres à virgules (float).
La fonction abs permet d'obtenir la valeur absolue d'un nombre entier. Utiliser cette
fonction pour calculer la valeur absolue de (a-b).
Exemple d'utilisation de abs :
S = abs (a-b);
La fonction ceil permet d'obtenir l'arrondi entier supérieur d'un nombre réel. Utiliser
cette fonction pour calculer l'arrondi supérieur de (a / b).
Exemple d'utilisation de ceil :
S = ceil (a/b);

BERTHOMIER Eric Page 3/624/03/00
Initiation au langage C.

Corrigés des exercices du chapitre 3

Soustraction de 2 nombres

#include <stdio.h>
#include <conio.h>
int main ()
{
int a,b; /* Déclaration des variables */
int d; /* Différence entre les 2 nombres */
a=0; /* Initialisation des variables */
b=0;
clrscr ();
printf("Calculatrice :\n\n");
printf("Valeur de a : ");
scanf("%d",&a);
printf("\n");
printf("Valeur de b : ");
scanf("%d",&b);
d=a-b;
printf("Valeur de a-b : %d\n",d); /* Affichage de la différence */
getch ();
return 0;
}
Multiplication de 2 nombres

#include <stdio.h>
#include <conio.h>
int main ()
{
int a,b; /* Déclaration des variables */
int m; /* Résultat de la multiplication */
a=0; /* Initialisation des variables */
b=0;
clrscr ();
printf("Calculatrice :\n\n");
printf("Valeur de a : ");
scanf("%d",&a);
printf("\n");
printf("Valeur de b : ");
scanf("%d",&b);
m = a*b;
printf("Valeur de a*b : %d\n", m);
/* Affichage de la multiplication */
getch ();
return 0;
}

BERTHOMIER Eric Page 4/624/03/00
Initiation au langage C.

Transformez les 3 précédents programmes avec le type float

Aucune difficulté dans cet exercice, je ne mettrai ici la correction que de la multiplication, les autres opérations se réalisent sur le même schéma.

int main ()
{
float a,b; /* Déclaration des variables */
float m; /* Résultat de la multiplication */
a=0; /* Initialisation des variables */
b=0;
clrscr ();
printf("Calculatrice :\n\n");
printf("Valeur de a : ");
scanf("%f",&a);
printf("\n");
printf("Valeur de b : ");
scanf("%f",&b);
m = a*b;
printf("Valeur de a*b : %f\n", m);
/* Affichage de la multiplication */
getch ();
return 0;
}
Pour l'addition :
m = a + b;
printf ("Valeur de a+b : %f", m);
Pour la soustraction :
m = a - b;
printf ("Valeur de a-b : %f", m);

Calculer la somme a + b + c

#include <stdio.h>
#include <conio.h>
int main ()
{
float a,b,c,s; /* Déclaration des variables */
a=0; /* Initialisation des variables */
b=0;

c=0; s=0;

clrscr ();
printf("Saisie de a : ");
scanf("%f",&a);
printf("Saisie de b : "); /* Saisie variables flottantes */
scanf("%f",&b);
printf("Saisie de c : ");
scanf("%f",&c);

BERTHOMIER Eric Page 5/624/03/00
Initiation au langage C.

s = a+b+c; /* Calcul de la somme */
printf("Valeur de s : %.2f\n",s); /* Affichage de la somme */
getch ();
return 0;
}
Calculer la différence a - b - c

d = a-b-c; /* Calcul de la différence */
printf("Valeur de d : %.2f\n",d); /* Affichage de la différence */
Calculer la multiplication a * b * c

m = a*b*c; /* Calcul de la multiplication */
printf("Valeur de m : %.2f\n",m); /* Affiche la multiplication */
Calculer la division a / b / c

d = a/b/c; /* Calcul de la division */
printf("Valeur de d : %.2f\n",d); /* Affichage de la division */
Calculer la valeur absolue de a - b

r=abs(a-b); /* Calcul de la valeur absolue */
printf("Valeur de r : %f\n",r); /* Affiche la valeur absolue */

Calculer l'arrondi de a + b

c=ceil(a/b); /* Calcul de l'arrondi */
printf("Valeur de c : %f\n",c); /* Affichage de l'arrondi */
Il aurait été possible d'utiliser %d du fait que l'arrondi est un nombre entier !

BERTHOMIER Eric Page 6/624/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_4.rtf

Conditions

Avec des Si on mettrait Paris en bouteille…
4.1 Exercice de mise en bouche

Ecrire un programme qui met en application le théorème de Pythagore pour calculer l'hypoténuse d'un triangle rectangle.

Rappel :
Dans un triangle rectangle, les longueurs côtés a +
² b
sont définies comme le montre la figure ci-contre. b ²
a
Note :
La racine carrée s'obtient par l'utilisation de la fonction sqrt (valeur)
contenue dans la bibliothèque math.h. (#include <math.h>)
a² peut s'obtenir par a*a.
Méthodologie :
Rechercher les variables nécessaires et les déclarer dans le programme.
Faire saisir a au clavier.
Faire saisir b au clavier.
Effectuer l'opération de la racine carrée et afficher le résultat.
4.2 Les conditions : Si Alors Sinon

if (condition vraie) si (condition vraie)
{alors
instructions 1 faire instructions 1
} }
else sinon
{ {
instructions 2 faire instructions 2
} }

Les conditions s'expriment avec des opérateurs logiques …
4.2.1 Opérateurs logiques relationnels

Ils servent à comparer deux nombres entre eux.

<
Supérieur >
Equivalent ==
Différent !=
Inférieur ou égal <=
Supérieur ou égal >=

BERTHOMIER Eric Page 1/524/03/00
Initiation au langage C.

4.2.2 Opérateurs logiques purs

Ce sont des opérateurs logiques permettant de combiner des expressions logiques.

Et (and) &&
Ou (or) ||
Non (not) !
N.B. | se nomme en informatique un pipe.
4.2.3 Vrai ou faux

La valeur Vrai peut être assimilée à la valeur numérique 1 ou à toute valeur > 0. La valeur Faux peut être assimilée à la valeur numérique 0.

L’opérateur Ou (||) correspond alors à une addition
Ou Vrai Faux +1 0
Vrai Vrai Vrai 12 1
Faux Vrai Faux 01 0
L’opérateur Et (&&) correspond alors à une multiplication
Et Vrai Faux *1 0
Vrai Vrai Faux 11 0
Faux Faux Faux 00 0
On notera que !Vrai = Faux et !Faux = Vrai.
4.2.4 Combinaison

Toutes les opérations logiques peuvent se combiner entre elles. La seule condition d'utilisation d'un si (if) avec de telles combinaisons est de l'entourer de ().

Exemple :
if ((car == 'a') || (car == 'A'))
4.2.5 Astuce

Vous verrez souvent ce type de code écrit :
if (er)
{
/* Alors faire quelque chose */
}
En appliquant ce qui a été vu précédemment, on en déduit que ce code signifie que si (er != 0) /* si er différent de 0 */
{
/* Alors faire quelque chose */
}

BERTHOMIER Eric Page 2/524/03/00
Initiation au langage C.

4.3 Les accolades

Les accolades entourant les blocs d'instructions d'une condition peuvent être omises si le bloc est constitué d'une seule instruction.

Exemple :
if (car == 'b')
printf ("car vaut b.");
else
printf ("car est différent de b.");
4.4 Exercices
Faites saisir une variable de type entière et indiquez à l'utilisateur si celle-ci est
Aide :
if (a>0)
{
printf ("Valeur positive");
}
else
{
printf ("Valeur négative");
}
Faites saisir une variable de type entière et indiquez à l'utilisateur si celle-ci est

Faites saisir une variable de type caractère et indiquez à l'utilisateur si celle-ci est
une voyelle.

BERTHOMIER Eric Page 3/524/03/00
Initiation au langage C.

Corrections des exercices du chapitre 4

Ecrire un programme qui effectue le théorème de Pythagore

#include <stdio.h>
#include <conio.h>
#include <math.h>
int main ()
{
floata; /* base du triangle */
floatb; /* côté du triangle rectangle */
floatp; /* valeur de l'hypoténuse (p pour Pythagore !) */
/* Initialisation des variables pour palier aux erreurs */
a = 0;
b = 0;
/* Effacer l'écran */
clrscr ();
/* Saisie de a */
printf ("Valeur de la base : ");
scanf ("%f",&a);
/* Saisie de b */
printf ("Valeur du côté : ");
scanf ("%f",&b);
/* Calcul par Pythagore */
p = sqrt (a*a + b*b);
/* Affichage du résultat */
printf ("L'hypoténuse mesure : %.2f",p);
/* Attendre avant de sortir */
getch ();
return (0);
}
Test du signe d'une valeur saisie au clavier

#include <stdio.h>
#include <conio.h>
#include <math.h>
int main ()
{
/* Valeur que l'on va saisir */
int a = 0;
/* Efface l'écran */
clrscr ();
/* Saisie de a */
printf("Saisie de a : ");

BERTHOMIER Eric Page 4/524/03/00
Initiation au langage C.

scanf("%f",&a);
/* Test condition a<0 */
if (a<0)
{
printf("la variable a est négative.\n");
}
else
{
/* Test condition a>0 */
if (a>0)
{
printf("la variable a est positive\n");
}
/* Sinon a est nulle */
else
{
printf("la variable a est nulle\n");
}
}
getch ();
return (0);
}
Test du signe d'une valeur saisie au clavier

#include <stdio.h>
#include <conio.h>
#include <math.h>
int main ()
{
/* Valeur que l'on va saisir */
char car;
/* Efface l'écran */
clrscr ();
/* Saisie du caractère a */
printf("Saisie du caractère : ");
scanf("%c",&car);
/* Test condition car voyelle minuscule */
if ((car == 'a') || (car == 'e') || (car == 'i') || (car == 'o') ||
(car == 'u') || (car == 'y') )
{
printf("la variable car est une voyelle.\n");
}
else
{
printf("la variable car est une consonne.\n");
}
getch ();
return (0);
}

BERTHOMIER Eric Page 5/524/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_5.rtf

Remise en forme

5.1 Prologue

L'objet de ce cours est de réaliser un petit break dans l'apprentissage du C et de s'attacher à voir que l'on est capable de réaliser avec le peu de moyen que l'on a.
Ce cours sera donc constitué de 3 exercices de difficultés croissantes avec apprentissage d'une nouvelle fonction et d'un exercice complet de programmation.
L'exercice 4 est assez difficile et mérite surtout de bien réfléchir avant de se mettre au clavier.

5.2 Exercice 1.

Réaliser un programme qui saisisse un nombre et indique à l'utilisateur si celui-ci est plus grand ou plus petit qu'un autre nombre fixé par le programme.

Exemple :
si (nbre_saisi<10)
alors "plus petit"
Reprendre l'exercice du chapitre 4 qui disait si un nombre est positif, négatif ou nul.
5.3 Retour sur Getch ()

La fonction getch ()permet d'attendre la frappe d'un caractère au clavier, de le lire et de le renvoyer. 2 utilisations peuvent être faites de getch (), la première est celle permettant d'attendre la frappe d'une touche sans se soucier de sa valeur, la seconde est celle permettant de lire un caractère au clavier.

Exemples:
Attente
getch ();
Saisie d'un caractère
char car;
car = getch ();
A chaque fois, getch () effectue le même traitement :
Attend la frappe d'une touche au clavier.
Renvoie le caractère frappé.
Dans le 1er cas, ce caractère n'est simplement pas récupéré.
5.4 Boucle Faire … Tant que (vrai)

Do … while, traduisez par Faire Tant que permet de réaliser une suite d'événements tant qu'une condition ou un ensemble de conditions est rempli.

Exemple
char car;

BERTHOMIER Eric Page 1/824/03/00
Initiation au langage C.

int sortie;
do
{
clrscr ();
printf ("Tapez S pour sortir …");

/* On saisit un caractère */ Suite d'évènements
car = getch ();
/* On le compare pour savoir si l'on peut sortir */
sortie = ((car == 's') || (car == 'S'));
while (!sortie);
Rappel :
Un nombre entier vaut la valeur logique vraie si celui-ci est différent de 0.
Un nombre entier vaut la valeur logique faux si celui-ci est égal à 0.
|| signifie un ou logique (or).
5.5 Exercice 2

Tapez l'exemple précédent, aménagez le, comprenez le, puis transformer le afin que l'on sorte de la boucle uniquement lorsque l'utilisateur a tapé le nombre 10.

Attention La saisie d'un nombre ne se fait pas par getch mais par scanf …
Cf. Chapitre 3.
5.6 Exercice 3
Voici un petit exemple de programme qui permet d'obtenir des nombres aléatoires entre
et 100.
Notes :
random et randomize sont définis dans la bibliothèque <stdlib.h>.

#include <stdio.h>
#include <stdlib.h>
int main()
{
int nb_alea; /* Nombre aléatoire */
clrscr ();
randomize ();

/* Le nombre aléatoire est stocké dans une variable puis affiché */ nb_alea = random (101);

printf ("%d",nb_alea);

/* Le nombre aléatoire est affiché directement à l'écran */ printf ("%d",random (101));

getch ();
}

BERTHOMIER Eric Page 2/824/03/00
Initiation au langage C.

randomize permet d'initialiser le système aléatoire.
random permet d'obtenir un nombre entre 0 et (n-1). [random (n)]

En vous aidant de ce petit programme et de ce qui a été fait précédemment, réaliser un petit jeu qui :

Initialise un nombre entre 0 et 100.
Tente de faire deviner ce nombre à l'utilisateur en lui indiquant s'il est plus petit ou
plus grand.
Pour cet exercice, aidez vous de ce qui a été fait précédemment.

5.7 Exercice 4 : jeu de 421

Vous allez réaliser un mini jeu de 421. Les règles sont simples, vous avez 4 essais pour réaliser avec les 3 dés un jet donnant 421. Le programme lance les dés et affiche le résultat. Si c'est un 421, on affiche "Gagné", sinon on demande à l'utilisateur d'appuyer sur la touche R pour relancer. Au bout du 4ème essai, au lieu de proposer de relancer, on affichera "Perdu" et le programme se terminera.

5.7.1 Analyse (ne pas toucher à l'ordinateur !).

Analyser l'énoncé du problème.
Identifier chaque étape du problème, ce qu'il faut faire pas à pas.
Analyser chaque étape en utilisant des mots français sans utiliser de terme
informatique afin d'avoir un fonctionnement pas à pas …

Faîtes comme si vous étiez un robot auquel on apprend comment jouer.

5.7.2 Programmation

Courage : Programmer le Jeu …

BERTHOMIER Eric Page 3/824/03/00
Initiation au langage C.

Corrigés des exercices du chapitre 5

§5.2

#include <stdio.h>
int main ()
{
int nb_choisi = 33;
int nb_saisi = 0;
clrscr (); /* Efface l'écran */
printf ("Votre nombre : ");
scanf ("%d",&nb_saisi);
if (nb_choisi < nb_saisi)
printf ("Mon nombre est plus petit");
else
{
if (nb_choisi == nb_saisi)
printf ("Mon nombre est égal");
else
printf ("Mon nombre est plus grand");
}
/* Attente */
getch ();
return (0);
}
§5.4

#include <stdio.h> #include <conio.h>

#include <stdlib.h>
int main ()
{
int valeur;
do
{
clrscr ();
printf ("Votre nombre : ");
scanf ("%d",&valeur);
}while (valeur != 10);
return (o);
}
§5.5
#include <stdio.h>
#include <stdlib.h> /* pour random */
#include <conio.h>
int main ()

BERTHOMIER Eric Page 4/824/03/00
Initiation au langage C.

{
int nb_hasard = 0;
int votre_nb = 0;
randomize ();

nb_hasard = random (101); /* Nombre entre 0 et 100 */ do

{
clrscr ();
printf("Saisie de votre nombre : ");
scanf("%d",&votre_nb);
if (nb_hasard < votre_nb)
{
printf ("\nMon nombre est plus petit");

/* A cause du clrscr () qui nous empêcherait de voir le message */

printf ("\nAppuyez sur une touche");
getch ();
}
else
{
if (nb_hasard > votre_nb)
{
/* il peut être aussi égal ... */
printf ("\nVotre nombre est plus grand");

/* A cause du clrscr () qui nous empêcherait de voir le message */
printf ("\nAppuyez sur une touche"); getch ();

}
}
}while (votre_nb != nb_hasard);
printf ("\nTrouvé");
getch ();
return (0);
}
Le jeu du 421 : version longue

#include <stdio.h>
#include <stdlib.h>
/* Note : true ou vrai est équivalent à la valeur 1 */
/* false ou faux est équivalent à la valeur 0 */
main()
{
int de1, de2, de3;
int gagne;
int essai;

essai = 0; gagne = 0;

BERTHOMIER Eric Page 5/824/03/00
Initiation au langage C.

randomize ();
do
{
clrscr ();

de1 = random (6) + 1; de2 = random (6) + 1; de3 = random (6) + 1;
printf ("Lancer : [%d] [%d] [%d]",de1,de2,de3); getch ();

if (de1 == 4)
{
if (de2 == 2)
{
if (de3 == 1)
gagne = 1;
else
gagne = 0;
}
else
{
if (de2 == 1)
{
if (de3 == 2)
gagne = 1;
else
gagne = 0;
}
else
gagne = 0;
}
}

if (de2 == 4)
{
if (de1 == 2)
{
if (de3 == 1)
gagne = 1;
else
gagne = 0;
}
else
{
if (de1 == 1)
{
if (de3 == 2)
gagne = 1;
else
gagne = 0;
}
else
gagne = 0;
}
}
if (de3 == 4)

BERTHOMIER Eric Page 6/824/03/00
Initiation au langage C.

{
if (de2 == 2)
{
if (de1 == 1)
gagne = 1;
else
gagne = 0;
}
else
{
if (de2 == 1)
{
if (de1 == 2)
gagne = 1;
else
gagne = 0;
}
else
gagne = 0;
}
}
essai = essai + 1;
}
while ((!gagne) && (essai != 4));
if (gagne)
printf ("\nGagné !!!");
else
printf ("\nPerdu ...");
getch ();
}
Le jeu du 421 : version courte
#include <stdio.h>
#include <stdlib.h>

main()
{
int de1, de2, de3;
int gagne;
int essai;
essai = 0;
randomize ();
do
{
clrscr ();

de1 = random (6) + 1; de2 = random (6) + 1; de3 = random (6) + 1;
printf ("Lancer : [%d] [%d] [%d]",de1,de2,de3); getch ();

gagne = ((de1 == 4) && (de2 == 2) && (de3 == 1));

BERTHOMIER Eric Page 7/824/03/00
Initiation au langage C.

gagne = gagne || ((de1 == 4) && (de2 == 1) && (de3 == 2)); gagne = gagne || ((de1 == 2) && (de2 == 1) && (de3 == 4)); gagne = gagne || ((de1 == 2) && (de2 == 4) && (de3 == 1));

gagne = gagne || ((de1 == 1) && (de2 == 2) && (de3 == 4));

gagne = gagne || ((de1 == 1) && (de2 == 2) && (de3 == 4)); essai = essai + 1;

}
while ((!gagne) && (essai != 4));
if (gagne)
printf ("\nGagné !!!");
else
printf ("\nPerdu ...");
getch ();
}

BERTHOMIER Eric Page 8/824/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_6.rtf

Et les shadoks pompaient …

6.1 While
De la même façon que Do While, il est possible d'utiliser While (condition == Vrai) …
Exemple:
char car;
while ((car != 's') && (car != 'S'))
{
car = getch ();
}
6.2 Et les shadoks apprenaient que reprendre équivaut à apprendre …

6.2.1 Exercice 1.1
Traduire en langage C, complétez avec les variables nécessaires, compilez, exécutez,
Faire
Effacez l’écran
Saisir une touche
Tant Que (touche != S) et (touche != s)
6.2.2 Exercice 1.2

Traduire en langage C, complétez avec les variables nécessaires, compilez, exécutez,
Faire
Effacez l’écran
Saisir un nombre
Tant Que (nombre != 10)
6.3 La fonction toupper ()
Le problème de la comparaison de la minuscule et de la majuscule peut être détourné

par l'utilisation de la fonction toupper qui transforme un caractère minuscule en majuscule. La fonction toupper s'utilise de la façon suivante :

char car;
char car_min;
car_min = 'a'
car = toupper (car_min);
printf ("%c",car);
affichera A

BERTHOMIER Eric Page 1/624/03/00
Initiation au langage C.

6.4 O tant que en emporte le shadok …

6.4.1 Exercice 2.1

Ecrire le programme…

Tant que je ne tape pas un nombre impair compris entre 1 et 10 je recommence la saisie d’un nombre.

6.4.2 Exercice 2.2

Ecrire le programme…
Tant que je ne tape pas une voyelle je recommence la saisie d’une touche.

6.5 Et les shadoks continuaient à pomper pour obtenir le résultat …
Dans les exercices qui suivent, la notion de compteur intervient. Un compteur est une
variable numérique que l'on décrémente (-1) ou incrémente (+1) suivant nos besoins.
Exemple :
int i;

i++; /* Incrémente le compteur défini par i */ i--; /* Décrémente le compteur défini par i */

i++; revient à la même chose que i=i+1;
i--; revient à la même chose que i = i-1;

Le nombre de caractères peut donc être comptabilisé en ajoutant 1 à une variable à chaque fois qu'une touche est frappée.

6.5.1 Exercice 3.1

Ecrire le programme…
Tant que je n’ai pas saisi 10 caractères, je recommence la saisie d’une touche.
6.5.2 Exercice 3.2
Ecrire le programme…
Tant que je n’ai pas saisi 10 nombres, je recommence la saisie d’un nombre.
6.6 Au Parc des Shadoks, on trie, voyelles, chiffres premiers …

6.6.1 Exercice 4.1

Ecrire le programme…
Tant que je n’ai pas saisi 10 voyelles, je recommence la saisie d’une touche.

6.6.2 Exercice 4.2

Ecrire le programme…

Tant que je n’ai pas saisi 10 chiffres premiers (1,3,5,7), je recommence la saisie d’un chiffre.

BERTHOMIER Eric Page 2/624/03/00
Initiation au langage C.

Corrigés des exercices du chapitre 6

Exercice 1.1

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char touche;
do
{
clrscr ();
a=getch ();
}while((touche!='S')&&(touche!='s'));
return (0);
}
Exercice 1.2

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int a = 0;
do
{
clrscr ();
printf("Saisie de a:");
scanf("%d",&a);
}while (a!=10);
return (0);
}
Exercice 2.1
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int a = 0;
do
{
clrscr ();
printf("Saisie de a:");
scanf("%d",&a);

while ( (a!=1) && (a!=3) && (a!=5) && (a!=7) && (a!=9)); return (0);

}

BERTHOMIER Eric Page 3/624/03/00
Initiation au langage C.

Exercice 2.2
Afin de faciliter la correction, j'utiliserai la fonction toupper qui
permet de transformer un caractère minuscule en majuscule.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char a;
do
{
clrscr ();
printf("Tapez une lettre:");
a=toupper (getch());
}

while((a!='A')&&(a!='E')&&(a!='I')&&(a!='O')&&(a!='U')&&(a!='Y')); return (0);

}
Exercice 3.1

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char car;
int nb_frappes = 0; /* nombre de caractères frappés */
do
{
clrscr ();
car = getch (); /* 1 caractère frappé */
nb_frappes ++;
while (nb_frappes != 10);
return (0);
}
Exercice 3.2

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int nbre;
int nb_frappes = 0; /* nombre de caractères frappés */
do
{
clrscr ();

BERTHOMIER Eric Page 4/624/03/00
Initiation au langage C.

printf ("Votre nombre : ");
scanf ("%d",&nbre); /* 1 nombre saisi */
nb_frappes ++;
while (nb_frappes != 10);
return (0);
}
Exercice 4.1

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char a;
int nb_voyelles = 0;
do
{
clrscr ();
printf("Tapez une lettre:");
a=toupper (getch());
if ((a=='A') || (a == 'E') || (a == 'I') || (a == 'O') || (a == 'U') || (a == 'Y'))
{
nb_voyelles ++;
}
while (nb_voyelles != 10);
return (0);
}
Exercice 4.2

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int nbre;
int nb_nbre_premiers = 0;
do
{
clrscr ();
printf("Votre nombre : ");
scanf ("%d",&nbre);

if ((nbre == 1) || (nbre == 3) || (nbre == 5) || (nbre == 7)) {

nb_nbre_premiers ++;
}
while (nb_nbre_premiers != 10);
return (0);

BERTHOMIER Eric Page 5/624/03/00
Initiation au langage C.

}

BERTHOMIER Eric Page 6/624/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_7.rtf

Gestion de l'écran

7.1 Prologue

Nous allons voir dans ce chapitre quelques fonctions permettant de se positionner et d'afficher à l'écran. Pour utiliser ces fonctions nous aurons besoin de la bibliothèque conio.h. (#include <conio.h>)

7.2 L'écran

7.2.1 Description

Un écran Dos se décompose de 80 colonnes et 25 lignes. Pour se positionner à un endroit
précis de l'écran, on utilise la fonction gotoxy (pos_x, pos_y);
pos_x allant de 1 à 80 et pos_y allant de 1 à 25.
L’origine est placée dans le coin haut gauche.

Origine Pos_x
(1,1)

Point
(40,12

Point
(80,25) Pour faire afficher un caractère ou une chaîne de caractère à cet endroit, on utilise printf.
Exemple :

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int main ()
{
clrscr ();
gotoxy (70,25);
printf ("Coucou");
getch ();
return (0);
}
7.2.2 Exercice d'application n°1

Faire afficher en (12,7) "Bonjour c'est moi et j'utilise le C" puis afficher en (23,18) "C'est bien on peut faire ce que l'on veut".

BERTHOMIER Eric Page 1/724/03/00
Initiation au langage C.

7.3 Choix multiple : switch … case

7.3.1 Définition

Switch … case permet l'exécution d'une série d'instructions dans le cas ou une variable a
On peut traduire switch … case par dans le cas où la variable vaut … faire …
En plus des cas où, il est possible d'ajouter le cas default qui signifie par défaut faire …

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
int main ()
{

char car; clrscr ();

gotoxy (1,1);
printf ("Saisie d'une touche : ");
car = getch ();
gotoxy (1,3);

switch (car)
{

/* Dans le cas où car est égal au caractère a,e,i,o,u ou y afficher "Voyelle" */

case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'y':
printf ("Voyelle\n");

/* Fin des instructions concernant un caractère a,e,i,o,u ou y */

break ;

/* Dans le cas où car est égal au caractère 1 afficher "Chiffre 1" */

case '1':
printf ("Chiffre 1\n");
break ;

/* Dans les autres cas afficher "Rien à faire pour ce cas" */

default :
printf ("Rien à faire pour ce cas\n");
break;
}
getch ();
return (0);
}

BERTHOMIER Eric Page 2/724/03/00
Initiation au langage C.

7.3.2 Exercice d'application n°2

Réaliser un programme qui fait afficher :
F "Gauche" lorsque l'on appuie sur g ou G
F "Droite" lorsque l'on appuie sur d ou D
F "Haut" lorsque l'on appuie sur h ou H
F "Bas" lorsque l'on appuie sur b ou B
7.4 Opérateurs unaires

Les opérateurs unaires (unary) du C sont des raccourcis mathématiques permettant de réaliser des opérations mathématiques ou logiques simples utilisant 2 fois la même variable. Ils facilitent aussi les calculs de la machine.

Exemple :
a = a + 5; s'écrit a+=5;
Quelques opérateurs
Opérateur Signification Exemple
a += val a = a + val a += 3
a -= val a = a – val a -= 7
a *= val a = a * val a *= 9
a /= val a = a / val a /= 11

Il existe bien d'autres opérateurs unaires notamment logique mais ceci dépassant l'étendue de ce cours, je vous renvoie aux livres qui traite cela très bien.

7.5 Exercice : animation d'un curseur à l'écran

Traduire en langage C, compilez, exécutez, comprenez …

Déclarer une variable x (initialisée à 10) de type entier. Déclarer une variable y (initialisée à 10) de type entier.
Ces variables représenteront les positions du curseur à l'écran. Déclarer une variable car de type caractère.
Déclarer une variable sortie de type entier (vrai ou faux en fait) initialisée à faux (0).

Faire
Effacer l'écran.
Afficher le caractère # en position x,y
Saisir un caractère sans écho et le mettre dans car.
Dans le cas où car est égal à :
G ou g : x = x – 1 (x--)
D ou d : x = x + 1 (x++)
H ou h : y = y –1 (y--)
B ou b : y = y + 1 (y++)
S ou s : sortie = vrai
Autre : réaliser un beep

BERTHOMIER Eric Page 3/724/03/00
Initiation au langage C.

Fin du Dans le cas où
Tant que (sortie = faux) /* ou !sortie */
Effacer l'écran
Aide :
Pour réaliser un beep, écrivez printf ("%c",0x07);
7.6 Complément d'exercice

Si l'on arrive au bord de l'écran (x<1, x> 80, y<1, y>25), faire en sorte que le curseur passe de l'autre côté de l'écran.

Si x < 1 alors x = 80
Si x > 80 alors x = 1

Si y < 1 alors y = 25 Si y > 25 alors y = 1

7.7 La cerise sur le gâteau …

Les touches de direction Haut Bas Droite Gauche sont en fait composées de 2 touches la touche de code Ascii 0 et respectivement les codes Ascii des touches H, P, K, M.
Pour pouvoir utiliser ces touches, il faut donc faire un double switch de la façon suivante :

switch (car)
{
case 0:
car = getch ();
switch (car)
{
case 'H':
y --;
break;
…
}
}
Complétez le programme pour qu'il fonctionne aussi avec les touches de direction.

BERTHOMIER Eric Page 4/724/03/00
Initiation au langage C.

Corrigés des exercices du Chapitre 7

Exercice d'application n°1

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
clrscr ();
gotoxy (12,7);
printf ("Bonjour, c'est moi et j'utilise le C");
gotoxy (23,18);

printf ("C'est bien, on peut faire ce que l'on veut"); getch ();

return (0);
}
Exercice d'application n°2
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char car;
clrscr ();
gotoxy (1,1);
printf ("Saisie d'une touche :");
car = getch ();
gotoxy (1,3);
switch (car)
{

case 'g': case 'G':

printf ("Gauche\n");
break;

case 'd': case 'D':

printf ("Droite\n");
break;

case 'h': case 'H':

printf ("Haut\n");
break;

case 'b': case 'B':

printf ("Bas\n");
break;
default:
printf ("Rien à faire pour ce cas\n");
break;

BERTHOMIER Eric Page 5/724/03/00
Initiation au langage C.

}
getch ();
return (0);
}
Exercice : animation d'un curseur à l'écran

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char car;

int x=0, y=0; int sortie=1;

clrscr ();
printf ("Saisie de x:\n");
scanf ("%d",&x);
printf ("Saisie de y:\n");
scanf ("%d",&y);
do
{
clrscr ();
gotoxy (x,y);

printf ("#"); car=getch ();

car = toupper (car); /* Evite le test minuscule/majuscule */
switch (car)
{
case 'G':
x--;
if (x<1)
x = 80;
break;
case 'D':
x++;
if (x>80)
x = 1;
break;
case 'H':
y--;
if (y<1)
y = 25;
break;
case 'B':
y++;
if (y>25)
y = 1;
break;
case 'S':

BERTHOMIER Eric Page 6/724/03/00
Initiation au langage C.

sortie=0;
break;
default:
printf ("%c",0x07);
break;
}
while (sortie==1);
return (0);
}
La cerise sur le gâteau …

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
char car;
int y=10,x=10;
int sortie = 0;
clrscr ();
do
{
car=getch ();
switch (car)
{
case 0:
car=getch();
switch(car)
{
case 'H':
y--;
break;
case 'P':
y++;
break;
case 'K':
x--;
break;
case 'M':
x++;
break;
}
break;

case 'S': case 's':

sortie = 1;
break;
}
clrscr ();
gotoxy (x,y);
cprintf ("*");
while (!sortie);
return (0);
}

BERTHOMIER Eric Page 7/724/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_8.rtf

Organigrammes et algorithmes.
8.1 Organigrammes.

Bien qu'inusité de nos jours, les organigrammes permettent d'avoir une approche visuel que ne fournissent pas les algorithmes. Un organigramme se définit comme une représentation graphique des structures de contrôle.

Nous ne verrons que les deux symboles suivants :

Le rectangle qui représente un bloc d'instructions. Le losange qui représente une condition : Si Alors Sinon …

8.1.1 If (Condition) Alors Instructions 1

Début

if (Condition) Condition Vrai Instructions 1 Faux Instructions 2
{
Instructions 1;
}

Instructions 2;
Fin

8.1.2 If (Condition) Alors Instructions 1 Else Instructions 2

Début

if (Condition) Condition Vrai Instructions 1 Faux Instructions 2
{
Instructions 1;

else
{
Instructions 2;
}
Fin

  • BERTHOMIER Eric Page 1/724/03/00

Initiation au langage C.

8.1.3 Do … While (Condition)

Début

do

  • Instructions

Instructions;
}
while (Condition); Condition Vrai
Faux
Fin

8.1.4 While (Condition)

Je ne détaillerai pas cet algorithme au vue qu'il est très peu différent de celui-ci dessus et constituera un excellent exercice.

8.1.5 Switch (variable)… Case …

Début
Switch(variable)
{
Case valeur 1: Variable == valeur 1 Instructions 1
Instructions 1;
Break;
Faux
Case valeur 2:
Instructions 2; Variable == valeur 2 Instructions 2
Break;
…

  • Faux

Fin

8.1.6 Conclusion sur les organigrammes

Les organigrammes permettent de construire des programmes à l'aide de dessins, ceci simplifie énormément la tache d'analyse du programmeur. Il a été maintenant remplacé par les algorithmes qui en deux mots peuvent être définis comme une transcription francisé de ce que l'on désire faire (pour i allant de 1 à 10 faire …). Les algorithmes seront vus succinctement dans l'initiation au C niveau 2.

  • BERTHOMIER Eric Page 2/724/03/00

Initiation au langage C.

8.2 Exercice 1 : Et les shadoks crièrent : "Pourquoi tant que on a pas tout compris, on continue ?"

Traduire en langage C, compilez, exécutez, comprenez …

  • Déclarez une variable nb_rech de type entier
  • Déclarez une variable nb_user de type entier
  • Déclarez une variable essai de type entier et l’assigner à la valeur 0
  • Déclarez une variable gagne de type entier et l’assigner à la valeur 0
  • Assignez à la variable nb_rech la valeur de random (101)
  • Effacez l’écran
  • Faire

{

  • Demandez et saisir un nombre entre 0 et 100. Soit nb_user la variable utilisée.
  • Si nb_user < nb_rech

{
Afficher « Le nombre à trouver est plus grand
essai = essai + 1
}
Sinon
{
Si (nb_user > nb_rech)
{
Afficher « Le nombre à trouver est plus petit
essai = essai + 1
}
Sinon
gagne = 1;
}
Tant que (essai != 5) et (!gagne);

  • Si (gagne)

Ecrire "Gagné"
Sinon
{
Ecrire "Perdu"
Faire afficher le nombre nb_rech
}

  • Attendre l’appui d’une touche

8.3 Et les shadoks découvrirent la couleur
La fonction textcolor (no_couleur) permet de changer la couleur du texte. Le no de
couleur (no_couleur) est un entier allant de 0 à 15. Pour pouvoir afficher en couleur, il
est nécessaire d'utiliser la fonction cprintf à la place de printf. Cprintf possède la même
syntaxe que printf.

  • BERTHOMIER Eric Page 3/724/03/00

Initiation au langage C.

Constante Couleur Valeur Couleur de Fond Couleur de caractère
BLACK Noir 0 Oui Oui
BLUE Bleu 1 Oui Oui
GREEN Vert 2 Oui Oui
CYAN Cyan 3 Oui Oui
RED Rouge 4 Oui Oui
MAGENTA Magenta 5 Oui Oui
BROWN Brun 6 Oui Oui
LIGHTGRAY Gris Clair 7 Oui Oui
DARKGRAY Gris Foncé 8 Non Oui
LIGHTBLUE Bleu Clair 9 Non Oui
LIGHTGREEN Vert Clair 10 Non Oui
LIGTHCYAN Cyan Clair 11 Non Oui
LIGHTRED Rouge Clair 12 Non Oui
LIGHTMAGENTA Magenta Clair 13 Non Oui
YELLOW Jaune 14 Non Oui
WHITE Blanc 15 Non Oui
BLINK Clignotant 128 Non ***

8.4 Utilisation du clignotement et des codes couleur.

Les valeurs du tableau précédent s'utilisent soit par leur nom, soit par leur code couleur. Ceci signifie que :

textcolor (2) est équivalent à textcolor (GREEN);
En fait GREEN est définit comme valant 2. (cf. #define dans Initiation au C niveau 2).

Pour faire clignoter du texte, il faut ajouter la valeur BLINK à la valeur de la couleur ce qui nous donne :

textcolor (2+128);
ou textcolor (GREEN+BLINK);
ou encore textcolor (130);
Exemple :
#include <conio.h>
int main ()
{
clrscr ();
textbackground (GREEN);
textcolor (RED);
cprintf ("Coucou");
getch ();
}
affiche "Coucou" en couleur rouge sur fond vert.
Test :
Au lieu de
clrscr ();

  • BERTHOMIER Eric Page 4/724/03/00

Initiation au langage C.

textbackground (GREEN);
écrivez :
textbackground (GREEN);
clrscr ();
Aucun commentaire ...
8.5 Exercice 2 :

Ecrivez un programme qui fait afficher "Je suis vert de peur" en bleu clignotant sur fond jaune poussin. Eh oui, pas shadok pour rien.

8.6 Exercice 3 : Et les shadoks créèrent un mini - yam …
Je dispose de 5 dés numérotés de 1 à 5.
Soient d1, d2, d3, d4, d5 ces 5 dés.
Utilisez la fonction Random pour afficher le tirage des 5 dés.
Affichez gagné si le résultat est un Yam (5 dés identiques).

  • BERTHOMIER Eric Page 5/724/03/00

Initiation au langage C.

Corrigés des exercices du chapitre 8

  • Exercice 1 : Et les shadoks crièrent : "Pourquoi tant que on a pas tout

compris, on continue ?"

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int nb_rech=0, nb_user=0;
int gagne=0;
int essai=0;
randomize ();
nb_rech= random (101); /* Nombre entre 0 et 100 */
clrscr ();
do
{
printf("Saisir un nombre entre 0 et 100 :");
scanf("%d",&nb_user);
if (nb_user<nb_rech)
{

printf("Le nombre à trouver est plus grand\n"); essai=(essai+1);

}
else
{
if(nb_user>nb_rech)
{

printf("Le nombre à trouver est plus petit\n"); essai=(essai+1);

}
else
{
gagne=1;
}
}
}while((essai!=5)&&(!gagne));
if (gagne)
printf("Gagné !");
else
printf("Perdu !");
printf("\nNombre recherché : %d",nb_rech);
getch ();
}

  • Exercice 2

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>

  • BERTHOMIER Eric Page 6/724/03/00

Initiation au langage C.

int main()
{
clrscr ();
gotoxy(20,20);
textbackground (YELLOW);
textcolor (BLUE + BLINK);
cprintf ("Je suis vert de peur");
getch ();
}

  • Exercice 3 : : Et les shadoks créèrent un mini - yam …

#include <stdio.h>
#include<stdlib.h>
int main ()
{
int d1,d2,d3,d4,d5;
randomize ();
/* Tirage des 5 dés */

d1 = random (6) + 1; d2 = random (6) + 1; d3 = random (6) + 1; d4 = random (6) + 1; d5 = random (6) + 1;

clrscr ();
/* Affichage du tirage */

printf ("\nD1 : %d, D2 : %d, D3 : %d, D4 : %d, D5 : %d" ,d1,d2,d3,d4,d5);

/* Résultat */
if ((d1 == d2) && (d2 == d3) && (d3 == d4) && (d4 == d5)) printf ("\nGagné !");
else
printf ("\nPerdu !");
getch ();
}

  • BERTHOMIER Eric Page 7/724/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_9.rtf

Les boucles

9.1 Et les shadoks pédalèrent pendant 15 tours …

Pour faire effectuer un certain nombre de fois une tâche on utilise l'instruction for de la façon suivante (avec i, une variable de type entier (int)).

for (i=point de départ; i<point d'arrivée; i=i+pas)
{
instruction(s) répétée(s);
}
Pour un souci de simplicité, nous dirons simplement que la formule suivante : for (i=0; i<15; i++)
{
instr ;
}
signifie que l'on va exécuter instr pour i valant 0 à 14 (<15) c'est à dire 15 fois.
Exemple:
#include <conio.h>
int main ()
{
int i;
clrscr ();
for (i=0; i<15; i++)
{
printf ("Je me répète pour i valant %d\n",i); }
printf ("Je me suis répété‚ 15 fois");
getch ();
}
9.2 Syntaxe
De la même façon que le if le for ne nécessite pas d'accolade si le nombre d'instructions
répéter est de 1.
Exemple :
On peut utiliser cette fonctionnalité dans le programme précédent en remplaçant :
for (i=0; i<15; i++)
{
printf ("Je me répète pour i valant %d\n",i); }
par
for (i=0; i<15; i++)
printf ("Je me répète pour i valant %d\n",i);

9.3 Exercice 1
Utilisez une boucle pour i variant de 0 à 15 inclus pour afficher :

BERTHOMIER Eric Page 1/624/03/00
Initiation au langage C.

"Ceci est la couleur i"
i est remplacé par sa valeur en position (1,i+1) avec la couleur i.
M cprintf ne supporte pas le \n permettant de retourner à la ligne; de ce fait utilisez i
allant de 0 à 15 pour modifier le no de ligne.
Aide

gotoxy (1,i+1); /* +1 car le coin haut gauche est aux coordonnées (1,1) */ textcolor (i);

cprintf ("Ceci est la couleur %d",i);
9.4 Notion de double boucle.

Il est possible de remplacer les instructions par une boucle afin de réaliser une double boucle.

On obtient donc :
Pour i allant de … à …
{
…
Pour j allant de … à …
{
…
}
}
Exemple :
#include <conio.h>
int main ()
{
int i;
int j;
clrscr ();
for (i=0; i<5; i++)
{

printf ("\nJe suis dans la boucle i, i vaut %d\n",i); for (j=3; j>0; j--)

{
printf ("Je suis dans la boucle j, j vaut %d\n",j); }
}
getch (); /* Attendre l'appui d'une touche */
}
9.5 Exercice 2:

En utilisant la double boucle, écrire un programme qui écrit 1 étoile, saute une ligne, écrit 2 étoiles, saute une ligne, écrit 3 étoiles … jusqu'à 5 étoiles afin d'obtenir ceci :

*
**
***

BERTHOMIER Eric Page 2/624/03/00
Initiation au langage C.

****
*****
9.6 Exercice 3 : Et les shadoks fêtèrent Noël …

9.6.1 "Cône" du sapin

A l'aide d'une double boucle et d'un gotoxy, réaliser un cône pour dessiner le haut du sapin en vert sur 12 lignes. Vous devriez obtenir ceci :

*
***
*****
sur 12 lignes …
Aide
Sur la ligne n° 1, on affiche 1 * 2 – 1 = 1 étoile en position (42-1,1)
Sur la ligne n° 2, on affiche 2 * 2 -1 = 3 étoiles en position (42-2,2)
Sur la ligne n° 3, on affiche 3 * 2 – 1 = 5 étoiles en position (42-3,3) Sur la ligne n° i, on affiche (i * 2 – 1) étoile en position (42–i, i); On obtient donc :
for (j=0; j<(i*2-1); j++)
{
gotoxy (42-i+j,i);
printf ("*");
}
9.6.2 Affichage du tronc

Pour poursuivre le sapin, il nous faut maintenant dessiner le tronc. Ecrire la suite du programme en dessinant le tronc à l'aide du caractère @. Vous devriez obtenir ceci (en brun) :
@@@ @@@ @@@

9.6.3 Affichage des boules de Noël
Afficher à l'aide de l'option blink des boules de Noël.
9.7 Exercice 4 : Table Ascii

Les codes Ascii (i.e. les nombres qui représentent les caractères en informatique) vont de 0 à 255. Ecrire un programme qui fait afficher à la suite (\n) puis de façon propre c'est à dire dans un tableau de 16 * 16 les codes Ascii avec les caractères qui leur correspondent.

Aide
Pour faire afficher le caractère associé à un code Ascii, on écrit :
printf ("%d : %c", code_ascii, code_ascii);
Exemple : int i = 65;
printf ("%d : %c", i, i); affichera 65 : A

BERTHOMIER Eric Page 3/624/03/00
Initiation au langage C.

Corrigés des exercices du chapitre 9

Exercice 1

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int i;
clrscr ();
for (i=0; i<=15; i++)
{
textbackground(15-i);
gotoxy (1,i+1);
textcolor (128+i);
cprintf ("Ceci est la couleur %d", i);
}
getch ();
}

Exercice 2

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{

int i; int j;

clrscr ();
for (i=1; i<5; i++)
{
for (j=1; j<=i; j++)
{
textcolor (11);
cprintf ("*");
}
printf ("\n");
}
getch ();
}
Exercice 3 : Et les shadoks fêtèrent Noël …
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main ()
{
int i;

BERTHOMIER Eric Page 4/624/03/00
Initiation au langage C.

int j;
clrscr ();
for (i=1; i<=12; i++)
{
for (j=1; j<= (i*2-1); j++)
{
gotoxy (41-i+j,i);

textcolor (2); cprintf ("*");

}
printf ("\n");
}
for (i=1; i<=3; i++)
{
for (j=3; j>=1; j--)
{
gotoxy (39+j,12+i);

textcolor (6); cprintf ("@");

}
}
textcolor (130);

gotoxy (41,3); cprintf ("*");

getch ();
}
Exercice 4 : Table Ascii

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void main ()
{
int i;
int j;
clrscr ();
for (i=0; i<255; i++)
{
printf ("%d -> %c\n",i,i);
}
getch ();
}
Attention, certains code Ascii provoquent des problèmes d'affichage …
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void main ()
{
int i;

BERTHOMIER Eric Page 5/624/03/00
Initiation au langage C.

int j;
int car = 0;
clrscr ();

printf (" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n"); for (i=1; i<=16; i++)

{
for (j=1; j<=16; j++)
{
printf ("%c ",car);
car ++;
}
printf ("\n ");
}
getch ();
}

BERTHOMIER Eric Page 6/624/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_1_10.rtf

10 Test d’assimilation des connaissances

Vous allez être placé dans les conditions d’un programmeur et non d’un analyste- programmeur. Ceci signifie que presque tous les algorithmes dont vous aurez besoin seront écrits et vous "n’aurez plus qu’à" les traduire. N'hésitez pas à consulter les cours et surtout les

10.1 Bibliothèques
Nous utiliserons la déclaration des bibliothèques suivantes :
#include <stdio.h>

#include <conio.h> #include <stdlib.h> #include <math.h> #include <time.h>

10.2 But du programme

Le programme que vous allez écrire consiste en une course de 3 chevaux dans laquelle l’utilisateur mise sur un des chevaux.

Aide : Essayez de bien comprendre ce qui est demandé et ce qui est écrit avant de
toucher votre clavier
10.3 Variables utilisées

/* saisie du pari */
char car; /* caractère saisi par l’utilisateur */
int sortie; /* booléen pour saisie du pari */
int pari; /* cheval sur lequel a parié le joueur */
int coul1,coul2,coul3; /* couleurs des chevaux */
/* dessin de la piste */
int i; /* variable de boucles */
int x1=0,x2=0,x3=0; /* La position des 3 chevaux */
int avance = 0; /* l’avance de chacun des chevaux */
time_t t; /* pour attente (partie offerte) */
int premier; /* cheval ayant gagné */
10.4 Saisie du no de cheval parié gagnant

Se positionner en (1,1)
Afficher : Sur quel cheval voulez vous parier (1,2 ou 3) ?
Faire
Sortie = 1 ;
Car = caractère saisi
Dans le cas où car est égal au caractère :

BERTHOMIER Eric Page 1/624/03/00
Initiation au langage C.

:
pari = 1;

coul1 = 2; coul2 = 1; coul3 = 1;

:
pari = 2;

coul1 = 1; coul2 = 2; coul3 = 1;

:
pari = 3;

coul1 = 1; coul2 = 1; coul3 = 2;

autrement :
sortie = 0
printf ("%c",0x7);
Tant Que sortie == 0
10.5 Dessin de la piste

Effacer l’écran

La piste sera dessinée grâce à deux lignes de 80 caractères – placées respectivement en ligne 10 et en ligne 14.

Utilisez une boucle for pour afficher la piste.
10.6 Course des chevaux

/* Initialisation des variables aléatoire */
randomize ();
/* Dessin des chevaux */
Faire
{
-> Effacement du cheval 1 (position précédente)
Position en (x1,11)
Ecrire (" ")

-> Affichage du cheval 1 Couleur de texte : coul1

-> Avance du cheval
x1 = x1 + nombre aléatoire de 1 à 6
Positionnement en x1,11
Affichage du caractère "1"

BERTHOMIER Eric Page 2/624/03/00
Initiation au langage C.

Faire la même chose pour le cheval 2 sur la ligne 12 avec la couleur coul2 et la position de ligne x2.

Aide : le cheval 1 était en ligne 11

Faire la même chose pour le cheval 3 sur la ligne 13 avec la couleur coul3 et la position de ligne x3.

/* Partie de code offerte permettant d’attendre un peu */
for (i=0; i<500; i++)
time (&t);
}
tant que aucun cheval n’a franchi la colonne 74.
Aide : le cheval a franchi la colonne 74 quand sa position est supérieure à 74.
10.7 Détermination du gagnant

Effectuez les différents tests pour connaître le cheval arrivé en premier. On affectera la valeur 1 à la variable premier si c’est le 1er cheval qui a gagné (x1 le plus grand), 2 pour le cheval 2 et 3 pour le cheval 3.

Aide : Le cheval arrivé en premier est celui dont la position est la plus grande.
10.8 Pari gagnant ou perdant
Si le cheval parié est le même que le cheval arrivé en premier, afficher : Bravo vous avez gagné.
Sinon
Désolé vous avez perdu.
Cheval n°… vainqueur
Attendre l’appui d’une touche.

BERTHOMIER Eric Page 3/624/03/00
Initiation au langage C.

Correction de l'exercice du chapitre 10

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

#include <math.h> #include <time.h>

int main ()
{
int i;
int x1=0,x2=0,x3=0;
int avance = 0;
time_t t;
int pari;
int premier;
int sortie;
int coul1,coul2,coul3;
char car;
/* Pari sur un cheval */
clrscr ();
gotoxy (1,1);

printf ("Sur quel cheval voulez vous parier (1,2 ou 3) ?"); /* Choix d'un cheval */

do
{
sortie = 1;
car = getch ();
switch (car)
{
case '1':
pari = 1;

coul1 = 2; coul2 = 1; coul3 = 1;

break;
case '2':
pari = 2;

coul1 = 1; coul2 = 2; coul3 = 1;

break;
case '3':
pari = 3;

coul1 = 1; coul2 = 1; coul3 = 2;

break;
default:
sortie = 0;
printf ("%c",0x7);
break;

BERTHOMIER Eric Page 4/624/03/00
Initiation au langage C.

}
while (!sortie);
/* Efface l'écran */
clrscr ();
/* Dessin de la piste */
for (i=1; i<=80; i++)
{
gotoxy (i,10);
printf ("-");
gotoxy (i,14);
printf ("-");
}
/* Initialisation des variables aléatoire */
randomize ();
/* Dessin des chevaux */
do
{

/* Effacement du cheval 1 (position précédente) */ gotoxy (x1,11);

printf (" ");
/* Affichage du cheval 1 */
textcolor (coul1);
x1 += random (6) + 1;
gotoxy (x1,11);
cprintf ("1");

/* Effacement du cheval 2 (position précédente) */ gotoxy (x2,12);

printf (" ");
/* Affichage du cheval 2 */
textcolor (coul2);
x2 += random (6) + 1;
gotoxy (x2,12);
cprintf ("2");

/* Effacement du cheval 3 (position précédente) */ gotoxy (x3,13);

printf (" ");
/* Affichage du cheval 3 */
textcolor (coul3);
x3 += random (6) + 1;
gotoxy (x3,13);
cprintf ("3");
/* Attente */
for (i=0; i<5000; i++)
time (&t);
}
while ((x1<74) && (x2<74) && (x3<74));
if ((x1>x2) && (x1>x3))
premier = 1;
else

BERTHOMIER Eric Page 5/624/03/00
Initiation au langage C.

{
if ((x2>x1) && (x2>x3))
premier = 2;
else
premier = 3;
}
gotoxy (1,14);
if (premier == pari)
printf ("\nBravo vous avez gagné.");
else
printf ("\nDésolé vous avez perdu.\nCheval n°%d vainqueur",premier);
getch ();
return (0);
}

BERTHOMIER Eric Page 6/624/03/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_2_1.rtf

Pointeurs et Fonctions

1.1 Variables : pointeurs et valeurs

1.1.1 Les variables et la mémoire.

Une variable (par exemple char car) est en fait une petite zone mémoire ici de 1 octet que l'on s'alloue et où l'on va ranger les informations (c'est la boîte vue durant le niveau 1). Chaque type de variable utilise au moins 1 octet. Le type int varie selon les compilateurs, pour nous il utilise 2 octets c'est à dire 2 cases mémoire. Nous avons vu précédemment qu'il était possible de voir le contenu d'une variable avec la fonction printf et qu'il était possible de mettre une valeur dans une variable avec l'opérateur = ou la fonction scanf.

Exemple :
char car = 'C';
printf ("%c",car);

Représentons-nous la mémoire comme une rue remplie de maisons. Chaque case mémoire est représentée par une maison.
Chaque maison porte un numéro, c'est ce que l'on appelle son adresse postale. Pour une case mémoire, on parlera d'adresse mémoire.
Cette adresse est unique pour chaque maison. Cependant, rien ne vous empêche de vous tromper intentionnellement ou non de maison. De la même façon, l'adresse mémoire d'une case est unique mais rien ne vous empêche de vous tromper intentionnellement ou non de case mémoire. (cf. Niveau 1)

Une adresse mémoire :
Exemple vie courante :
Mr Leneuf adresse : 99, grand rue
Exemple en C :
char car adresse : 0x001F (soit 31 en décimal) 0x signifie adresse hexadécimale (ou en base 16).
En langage C, l'adresse mémoire est accessible en faisant précéder la variable de
l'opérateur &.
Exemple :

char car = 'C'; int nbre = 12;

printf ("Adresse de car : %ld [%lx]",&car, &car);
printf ("Adresse de nbre : %ld [%lx]",&nbre, &nbre);
On ajoute "l" devant le d (%ld) et devant le x (%lx) afin de faire afficher un entier long.

Berthomier Eric Page 1/12 01/09/00
Initiation au langage C.
Niveau 2.
1.1.2 Pointeurs

Pour utiliser les adresses mémoire des variables à la place des variables elles-mêmes, on utilise les pointeurs. Les pointeurs sont définis par un type et se déclarent de la façon suivante :

type* variable;
ou
type *variable;
Le signe * indique l'utilisation d'un pointeur i.e. l'utilisation d'une adresse mémoire. Le type permet simplement de savoir comment le C doit interpréter l'adresse mémoire lors de sa lecture. Lors de l'utilisation des chaînes de caractères nous en verrons des exemples.
Pour enregistrer une valeur dans une adresse mémoire on écrit :
*variable = <valeur>
*variable signifie contenu de l'adresse mémoire.
Exemple:

#include <stdio.h> #include <stdlib.h>

int main ()
{
char car='C';
char* ptr_car = NULL;
clrscr ();
printf ("Avant, le caractère est : %c",car);
ptr_car = &car; /* ptr_car = adresse de car */
*ptr_car = 'E'; /* on modifie le contenu de l'adresse mémoire */

printf ("\nAprès le caractère est : %c",car); /* i.e. on a modifié car */ getch ();

return (0);
}
NULL signifie l'adresse 0. Il indique que l'adresse mémoire est invalide.
Explicitons cet exemple à l'aide de notre rue bordée de maison.
ptr_car = &car; Signifie que l'on m'a donné l'adresse postale de M. car. *ptr_car = 'E'; Signifie que je rentre chez M. car et que j'y dépose le caractère E. printf ("%c",car); Signifie que l'on va lire ce qu'il y a dans la maison de M. et qu'on le fait afficher.
1.1.3 Exercices d'application
Réaliser un programme équivalent qui change la valeur en K.
Réaliser un programme équivalent qui change une valeur numérique (int) de 10 à 35.

Berthomier Eric Page 2/12 01/09/00
Initiation au langage C.
Niveau 2.
1.2 Les fonctions

1.2.1 Définition générale

Une fonction est un petit bloc de programme qui à l'image d'une industrie va créer, faire ou modifier quelque chose.
Un bloc de programme est mis sous la forme d'une fonction si celui-ci est utilisé plusieurs fois dans notre code (dans notre programme) ou simplement pour une question de clarté (Imaginez un livre sans paragraphe).
A l'identique de notre fonction principale main (), une fonction s'écrit de la façon suivante :
<type de sortie> <nom de la fonction> (<paramètres d'appels>) {

Déclaration des variables internes à la fonction
Corps du programme
Retour
}

1.2.2 Void

Le type void signifie indéfini, il est utilisé :
comme type de sortie pour les fonctions qui ne retournent aucun résultat.
dans la déclaration de pointeurs sur des zones mémoires dont on ne connaît pas
le type.
Exemples
/* Pointeur sur une zone mémoire indéfinie */
void*m_ptr;
/* Fonction bête affichant un caractère */
void Affiche_car (char car)
{
printf ("%c",car);
}
1.2.3 Variables globales et locales
Les variables déclarées dans les fonctions sont dites locales. Il existe aussi les
variables dites globales qui sont déclarées en dehors de toute fonction y compris le
main ().
Les variables globales sont modifiables et accessibles par toutes les fonctions sans

avoir besoin de les passer en paramètre. Il est de ce fait extrêmement dangereux d'utiliser des variables globales.

Berthomier Eric Page 3/12 01/09/00
Initiation au langage C.
Niveau 2.
Les variables locales ne sont modifiables et accessibles que dans la fonction où elles

sont déclarées. Pour les modifier ou les utiliser par une autre fonction, il est nécessaire de les passer en paramètres.

Exemple de variable locale
int carre (int val)
{
int val_retour = 0; /* Déclaration variable locale */
val_retour = val * val;
return (val_retour);
}
void main ()
{
int val_retour = 0; /* Déclaration variable locale */
val_retour = carre (2);
printf ("Le carré de 2 est : %d", val_retour );
getch ();
}
val_retour est dans les deux cas une variable locale. Bien qu'elles aient le même nom
dans la fonction main et carre, les deux variables n'ont rien à voir entre elles .
Exemple de variable globale
"""" Il est fortement déconseillé d'utiliser les variables globales. """"
#include …
int val = 0; /* Déclaration variable globale */
int carre ()
{
int val_retour = 0;
val_retour = val * val;
return (val_retour);
}
void main ()
{
val = 2;
carre ();
printf ("Le carre de 2 est %d", carre ());
}
val est une variable globale.
On constate que le programme devient rapidement illisible …

Berthomier Eric Page 4/12 01/09/00
Initiation au langage C.
Niveau 2.
1.2.4 Utilisation et modification de données dans les fonctions
L'appel d'une fonction peut s'effectuer à l'aide de paramètres.
Ces paramètres peuvent être utilisés en les déclarant dans les parenthèses
Exemple
void Affiche_Coucou (int x, int y)
{
gotoxy (x,y);
printf ("coucou");
}
void main ()
{
Affiche_coucou (10,12);
getch ();
}
Ces paramètres peuvent être modifier à la condition qu'ils soient passés par adresse
c'est à dire que la fonction reçoive un pointeur.
Exemple
void Avance_Position (int* x)
{
*x = *x + random (6);
}
void main ()
{

int i=0; int x=0;

printf ("Position actuelle : %d",x);
for (i = 0; i<5; i++)
{
Avance_Position ( &x);
printf ("Nouvelle position : %d",x);
}
getch ();
}
1.2.5 Piège !

Attention, les opérateurs unaires (avec une seule opérande) sur les pointeurs sont dangereux.

En effet :
int* x;
*x++;

Berthomier Eric Page 5/12 01/09/00
Initiation au langage C.
Niveau 2.

augmentera l'adresse de x (on va chez le voisin) et non la valeur, pour cela il faut écrire :

(*x)++;
Ceci est une faute courante, prenez garde ...
1.2.6 Aide pour l’éditeur Turbo C 2.0

Le Turbo C permet de réaliser des copier coller dont vous aurez besoin dans les exercices suivants. Pour se faire, il faut sélectionner un bloc (un bloc est un morceau de texte de l’éditeur, il est marqué en surbrillance).
Note : Ctrl Lettre Lettre s’opère en appuyant sur la touche Ctrl et sans lâcher cette touche en appuyant successivement sur les deux lettres.

Pour sélectionner un bloc :
Utilisez la combinaison de touches Ctrl K B, pour marquer le début du bloc.
Utilisez la combinaison de touches Ctrl K K pour marquer la fin du bloc.
Votre zone est maintenant en surbrillance.
Pour désélectionner un bloc :
Utilisez la combinaison de touches Ctrl K K au début du bloc en surbrillance.

Positionnez vous à l’endroit où vous désirez faire la copie et faîtes Ctrl K C Pour déplacer faîtes Ctrl K V

1.2.7 Exercices
Réalisez une fonction Avance_cheval qui :
prendra pour paramètres la position x, la position y, la couleur et le caractère
représentant le cheval.
avancera le cheval en l'effaçant, le dessinant et mettant à jour sa position.
Augmenter le nombre de chevaux à 5.

Berthomier Eric Page 6/12 01/09/00
Initiation au langage C.
Niveau 2.
Correction de l'exercice du chapitre 1

En gras apparaissent les principaux changements opérés depuis le programme du niveau 1
chapitre 10.
Exercices 1.1.3
#include <stdio.h>
#include <stdlib.h>
int main ()
{
char car='C';
char* ptr_car = NULL;
clrscr ();
printf ("Avant, le caractère est : %c",car);
ptr_car = &car;
*ptr_car = 'K';
printf ("\nAprès le caractère est : %c",car);
getch ();
return (0);
}
#include <stdio.h>
#include <stdlib.h>
int main ()
{
int val = 10;
int *ptr_val = NULL;
clrscr ();
printf ("Avant, la valeur est : %d",val);
ptr_val = &val;
*ptr_val = 35;
printf ("\nAprès la valeur est : %d",val);
getch ();
return (0);
}
Fonction Avance_cheval
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

#include <math.h> #include <time.h>
void Avance_cheval (int *pos_x, int pos_y, int coul, char cheval) {

/* Effacement du cheval (position précédente) */
gotoxy (*pos_x,pos_y);
printf (" ");
/* Affichage du cheval */
textcolor (coul);

Berthomier Eric Page 7/12 01/09/00
Initiation au langage C.
Niveau 2.
*pos_x += random (6) + 1;
gotoxy (*pos_x,pos_y);
cprintf ("%c",cheval);
}
int main ()
{
int i;
int x1=0,x2=0,x3=0;
time_t t;
int pari;
int premier;
int sortie;
int coul1,coul2,coul3;
char car;
/* Pari sur un cheval */
clrscr ();
gotoxy (1,1);

printf ("Sur quel cheval voulez vous parier (1,2 ou 3) ?"); /* Choix d'un cheval */

do
{
sortie = 1;
car = getch ();
switch (car)
{
case '1':
pari = 1;

coul1 = 2; coul2 = 1; coul3 = 1;

break;
case '2':
pari = 2;

coul1 = 1; coul2 = 2; coul3 = 1;

break;
case '3':
pari = 3;

coul1 = 1; coul2 = 1; coul3 = 2;

break;
default:
sortie = 0;
printf ("%c",0x7);
break;
}
while (!sortie);
/* Efface l'‚cran */
clrscr ();

Berthomier Eric Page 8/12 01/09/00
Initiation au langage C.
Niveau 2.
/* Dessin de la piste */
for (i=1; i<=80; i++)
{
gotoxy (i,10);
printf ("-");
gotoxy (i,14);
printf ("-");
}
/* Initialisation des variables aléatoire */
randomize ();
/* Dessin des chevaux */
do
{
Avance_cheval (&x1, 11, coul1, '1');
Avance_cheval (&x2, 12, coul2, '2');
Avance_cheval (&x3, 13, coul3, '3');
/* Attente */
for (i=0; i<5000; i++)
time (&t);
}
while ((x1<74) && (x2<74) && (x3<74));
if ((x1>x2) && (x1>x3))
premier = 1;
else
{
if ((x2>x1) && (x2>x3))
premier = 2;
else
{
if ((x3>x1) && (x3>x2))
premier = 3;
else
premier = 0 ;
}
}
gotoxy (1,14);
if (premier == pari)
printf ("\nBravo vous avez gagné.");
else
{
if (premier == 0)
printf ("\nDésolé il y a égalité");
else
printf ("\nDésolé vous avez perdu.\nCheval n°%d vainqueur",premier);
}
getch ();
return (0);
}
Nous pouvons remarquer que le programme devient plus propre, plus clair.
5 chevaux

#include <stdio.h>

Berthomier Eric Page 9/12 01/09/00
Initiation au langage C.
Niveau 2.
#include <conio.h>
#include <stdlib.h>

#include <math.h> #include <time.h>
void Avance_cheval (int *pos_x, int pos_y, int coul, char cheval) {

/* Effacement du cheval (position pr‚c‚dente) */
gotoxy (*pos_x,pos_y);
printf (" ");
/* Affichage du cheval */
textcolor (coul);
*pos_x += random (6) + 1;

gotoxy (*pos_x,pos_y); cprintf ("%c",cheval);

}
int main ()
{
int i;
int x1=0,x2=0,x3=0,x4=0,x5=0;
time_t t;
int pari;
int premier;
int sortie;
int coul1,coul2,coul3,coul4,coul5;
char car;
/* Pari sur un cheval */
clrscr ();
gotoxy (1,1);
printf ("Sur quel cheval voulez vous parier (1,2,3,4 ou 5) ?");
/* Choix d'un cheval */
do
{
sortie = 1;
car = getch ();
switch (car)
{
case '1':
pari = 1;

coul1 = 2; coul2 = 1; coul3 = 1;

coul4 = 1;
coul5 = 1;
break;
case '2':
pari = 2;

coul1 = 1; coul2 = 2; coul3 = 1;

coul4 = 1;
coul5 = 1;
break;

Berthomier Eric Page 10/12 01/09/00
Initiation au langage C.
Niveau 2.
case '3':
pari = 3;

coul1 = 1; coul2 = 1; coul3 = 2;

coul4 = 1;
coul5 = 1;
break;
case '4':
pari = 4;
coul1 = 1;
coul2 = 1;
coul3 = 1;
coul4 = 2;
coul5 = 1;
break;
case '5':
pari = 5;
coul1 = 1;
coul2 = 1;
coul3 = 1;
coul4 = 1;
coul5 = 2;
break;
default:
sortie = 0;
printf ("%c",0x7);
break;
}
while (!sortie);
/* Efface l'‚cran */
clrscr ();
/* Dessin de la piste */
for (i=1; i<=80; i++)
{
gotoxy (i,10);
printf ("-");
gotoxy (i,16);
printf ("-");
}
/* Initialisation des variables aléatoire */
randomize ();
/* Dessin des chevaux */
do
{

Avance_cheval (&x1, 11, coul1, '1'); Avance_cheval (&x2, 12, coul2, '2'); Avance_cheval (&x3, 13, coul3, '3');

Avance_cheval (&x4, 14, coul4, '4');
Avance_cheval (&x5, 15, coul5, '5');
/* Attente */
for (i=0; i<5000; i++)

Berthomier Eric Page 11/12 01/09/00
Initiation au langage C.
Niveau 2.
time (&t);
}
while ((x1<74) && (x2<74) && (x3<74) && (x4<74) && (x5<74));
if ((x1>x2) && (x1>x3) && (x1>x4) && (x1>x5))
premier = 1;
else
{
if ((x2>x1) && (x2>x3) && (x2>x4) && (x2>x5))
premier = 2;
else
{
if ((x3>x1) && (x3>x2) && (x3>x4) && (x3>x5)) premier = 3;
else
{
if ((x4>x1) && (x4>x2) && (x4>x3) && (x4>x5)) premier = 4;
else
{
if ((x5>x1) && (x5>x2) && (x5>x3) && (x5>x4)) premier = 5;
else
premier = 0;
}
}
}
}
gotoxy (1,17);
if (premier == pari)
printf ("\nBravo vous avez gagné.");
else
{
if (premier == 0)
printf ("\nDésolé il y a égalité entre deux chevaux."); else
printf ("\nDésolé vous avez perdu.\nCheval n°%d vainqueur",premier);
}
getch ();
return (0);
}

Les test opérés à la fin du programme ne sont pas très réjouissants mais nous sommes là pour apprendre le C et non les méthodes d’optimisation.

Berthomier Eric Page 12/12 01/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_2_2.rtf

Tableaux & chaînes de caractères

2.1 Tableaux

2.1.1 Définition

Un tableau est un ensemble d'éléments consécutifs. Celui-ci peut être constitué de plusieurs lignes et colonnes. Nous n'utiliserons dans un premier temps que les tableaux

une seule ligne.
Exemple de tableau de caractères
AB CD E F G H
case 0 case 1 case 2 case 3 case 4 case 5 case 6 case 7 Les cases d'un tableau sont numérotées à partir de 0.
2.1.2 Déclaration
Un tableau se déclare de la manière suivante :
<type> <nom du tableau> [<taille du tableau>];
Exemples
Déclaration d'un tableau de 10 caractères.
char tab_char [10];
Déclaration d'un tableau de 10 nombres.
int tab_int [10];
Un tableau à plusieurs lignes se déclare de la façon suivante :
<type> <nom du tableau> [<taille 1ère dimension 1>][<taille 2nde dimension>] …;
Exemple
int table [5] [5]; représente un tableau d’entiers de 5 lignes * 5 colonnes.
2.1.3 Utilisation
On accède à un tableau en l'appelant par son nom et son numéro de case.
Exemple
Nom : tab_char Case : 3
tab_char [3] = 'C';
Nom : tab_int Case : 6
tab_int [6] = 10;
Attention : """" Aucune limite n'est fixée sur un tableau """" ,
il vous est donc possible d'écrire à l'extérieur de votre tableau donc chez le voisin …
c’est l’un des bugs le plus courant de l’informatique

Berthomier Eric Page 1/9 01/09/00
Initiation au langage C.
Niveau 2.
2.1.4 Cas spécifique des chaînes de caractères

Les chaînes de caractères sont des tableaux de caractères suivis d'un 0 binaire (ne pas confondre avec le caractère 0, nous parlons ici du code ascii) qui est considéré lui aussi comme un caractère. Une chaîne s'écrit donc : chaîne + 0.

Exemple
Eric s'écrit dans un tableau de 5 caractères de la façon suivante E r i c 0. Er i c 0
0123 4
2.1.5 Déclaration d'une chaîne de caractères

Une chaîne de caractères se déclare sous la forme d'un tableau de caractères de longueur fixe. Attention, comme signalé auparavant, si vous dépassez la longueur de tableau, vous écrivez chez le copain …

Exemple
char m_chaine [20];

permettra d'enregistrer des chaînes de 19 caractères maximum (20-1 pour le 0 de fin de chaîne).
D'autre part, il est possible de déclarer une chaîne de caractères sans en spécifier la longueur de départ de la façon suivante :

char chaine [] = "Eric";

C'est pratique dans la condition où l'on ne doit pas la réutiliser (on ne connaît sa taille que par son contenu).

2.1.6 Ecriture dans une chaîne de caractères

La première méthode permet de déclarer une chaîne mais non de l'initialiser. Pour se faire, une méthode consiste à utiliser la fonction sprintf de la façon suivante :

sprintf (<variable de type chaîne de caractères>, "%s","<valeur d'init.>");
Exemple
sprintf (m_chaine, "%s","Eric"); remplira m_chaine par Eric0.
(Le 0 n'est là que pour vous rappeler que l'on a besoin du 0 pour terminer la chaîne)
2.1.7 Affichage d'une chaîne de caractères

Une chaîne de caractères s'affiche grâce à la commande printf et le mot clé %s.
Exemple
printf ("%s",chaine);
2.1.8 Longueur d’un chaîne

La longueur d’une chaîne de caractères s’obtient par la fonction strlen. Le 0 de fin de
chaîne n’est pas compté dans cette longueur.
Exemple
char ch [] = "toto" ;
…
printf ("La longueur de %s est : %d", ch, strlen (ch)) ;

Berthomier Eric Page 2/9 01/09/00
Initiation au langage C.
Niveau 2.
Affichera 4 à l’écran.
2.1.9 Exercices
En utilisant une boucle (for),
Remplissez un tableau de 10 caractères avec les lettres de l'alphabet en
commençant par A (code ascii 65).
Faîtes afficher la chaîne de caractères ainsi obtenue (n'oubliez pas de rajouter le 0).
Faîtes afficher chaque caractère du tableau sous la forme "Caractère n° 0 : A".
Rappel :
Je peux écrire Tab_car [i] = code_ascii; où code_ascii est un entier représentant le code Ascii du caractère désigné.
Aide :
Pour faire afficher 1 caractère, on utilisera une syntaxe du style
int pos /* Position dans le tableau */
printf ("Caractère n° %d : %c",pos, Tab_car [pos]);
Un élément de tableau peut être assigné à la valeur 0 de la façon suivant : Tab_car [la bonne position] = 0; .

2.2 Gets : Saisie d'une chaîne de caractères

2.2.1 Description
La fonction gets permet de saisir une chaîne de caractère validée par un Retour
Chariot.
Attention, bien que cette chaîne nécessite d’être validée par un retour chariot, celui-ci
n'est pas enregistré dans le tableau de caractères.
2.2.2 Exemple d'utilisation
#include <stdio.h>
void main( void )
{
char line[81];
/* 81 : taille arbitraire supposée suffisante */
/* Une ligne écran = 80 caractères + 1 case pour le 0 de fin de chaîne */ printf( "Saisissez une chaîne de caractère :\n" );
gets( line );

/* La frappe de l’utilisateur sera enregistrée dans line, on suppose qu’il ne frappera pas plus de 80 caractères sinon aïe aïe aïe */
printf( "\nLa chaîne de caractères saisie est : \n%s\n", line ); printf ("Notons qu’il n’y a qu’un retour chariot.") ;

}
Exemple d'exécution
Saisissez une chaîne de caractère :
Bonjour !

Berthomier Eric Page 3/9 01/09/00
Initiation au langage C.
Niveau 2.

La chaîne de caractère saisie était :
Bonjour !
Notons qu’il n’y a qu’un retour chariot.
2.2.3 Passage d'une chaîne de caractères en paramètres

Il est bien agréable de pouvoir utiliser cette fonction mais son efficacité est très limitée. Nous allons donc créer notre propre fonction de saisie. Pour cela, il est nécessaire de passer en paramètre une chaîne de caractères.

Ceci s'effectue de la façon suivante :
int ma_saisie (char* chaine)
{
Faire ce qu'il faut …
return (0);
}
void main ()
{
char ma_chaine [30];
ma_saisie (ma_chaine);
return ();
}
On ne passe pas ici par le caractère & car en fait un tableau est une adresse mémoire sur
une suite de cases. Par contre, on peut passer par & en faisant ma_saisie (&chaine [0]),

&chaine [0] représentant l'adresse mémoire de la première case mémoire du tableau C’est un peu compliqué mais il suffit de se dire, je passe un tableau donc un ensemble de cases mémoire, donc je ne mets pas de &. Je transmets une case donc un élément d’un type comme un autre donc je mets le &.

2.3 Bug en chaînes …

.Reprenez l'exercice 2.1.7 et mettez 12 caractères dans votre tableau de 10 cases. .Essayez avec 80 caractères.
On peut facilement dire que le cas de dépassement de la taille d'un tableau est le bug le plus fréquent rencontré en programmation C alors prenez gare …

N.B. Ce problème n'existe pas sous Turbo Pascal mais on ne peut pas concilier sécurité

et rapidité ce qui fait que le C est de loin plus rapide que Turbo Pascal. Il suffit de s'imaginer qu'il est nécessaire d'effectuer un test à chaque assignation d'un élément du tableau !!!

2.4 Exercices
Réaliser votre propre fonction de saisie de chaîne de caractères …
Paramètres d'entrée : position x, y, chaîne de caractères, nombre de caractères
maximum.

Berthomier Eric Page 4/9 01/09/00
Initiation au langage C.
Niveau 2.

Algorithme :
/* Amorçage de la boucle */
car = saisie d'un caractère
tant que
(car est différent d'un retour chariot (0x0D)) ET
(nombre de caractères saisis < nombre de caractères maximum)) faire
ajouter car dans le tableau
afficher car à l'écran

mettre à jour les positions dans le tableau et sur l'écran car = saisie_car

fin du tant que
ajouter le 0 binaire de fin de chaîne.

Ajouter à votre fonction de saisie de caractères la gestion des touches backspace (code Ascii 8) et flèche gauche (code Ascii 0 suivi du code Ascii 75). Ces touches supprimeront le caractère qui précède en l'effaçant.

Pour vous aider, reportez-vous au chapitre 4 du cours Niveau 1.

Berthomier Eric Page 5/9 01/09/00
Initiation au langage C.
Niveau 2.
Correction des exercices du chapitre 2

Boucle for

En utilisant une boucle (for),

Remplissez un tableau de 10 caractères avec les lettres de l'alphabet en commençant par A (code ascii 65).
Faîtes afficher la chaîne de caractères ainsi obtenue (n'oubliez pas de rajouter le 0). Faîtes afficher chaque caractère du tableau sous la forme "Caractère n° 0 : A".

#include <stdio.h>
#include <stdlib.h>
void main ()
{

char tableau [11]; /* 10 caractères + 0 de fin de chaîne */ int i=0; /* compteur */

clrscr ();
/* Remplissage du tableau avec les caractères */
for (i=0; i<10; i++)
tableau [i] = 65 + i;
/* Ajout du 0 de fin de chaine */
tableau [10] = 0;
/* Affichage de la chaîne */
printf ("Tableau : %s\n",tableau);
/* Saut d'une autre ligne */
printf ("\n");
/* Affichage de chacun des caractères */
for (i=0; i<10; i++)
{
printf ("Caractère n°%d : %c\n",i,tableau [i]); }
/* Attente de la saisie d'une touche */
getch ();
}
Fonction de saisie (1ère partie)
Réaliser votre propre fonction de saisie de chaîne de caractère …

Paramètres d'entrée : position x, y, chaîne de caractères, nombre de caractères maximum.

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void saisie (int x, int y, char *chaine, int max_car)
{

Berthomier Eric Page 6/9 01/09/00
Initiation au langage C.
Niveau 2.

char car = ' '; int nb_car = 0;

gotoxy (x,y);
car = getch ();
while ((car != 0x0D) && (nb_car < max_car))
{
/* Ajout du caractère dans le tableau */
chaine [nb_car] = car;
/* Affichage … l'écran */
printf ("%c",chaine [nb_car]);

/* Mise … jour de la position dans le tableau */ nb_car ++;

/* Mise à jour de la position … l'écran */
x++;
gotoxy (x,y);
/* Saisie d'un nouveau caractère */
car = getch ();
}
chaine [nb_car] = 0;
}
void main ()
{
char chaine [11];
char message [] = "Votre saisie : ";
clrscr ();
gotoxy (1,3);
printf ("%s", message);

/* 10 : Laisser de la place pour le 0 binaire de fin de chaîne */ saisie (strlen (message),3,chaine,10);

/* Affichage du résultat */
gotoxy (1,5);
textcolor (GREEN);
cprintf ("Votre saisie : %s",chaine);
/* Attente pour visualisation */
getch ();
}
Fonction de saisie (2nde partie)

Ajouter à votre fonction de saisie de caractères la gestion des touches backspace (code Ascii ) et flèche gauche (code Ascii ). Ces touches supprimeront le caractère qui précède en l'effaçant.

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void beep ()
{

Berthomier Eric Page 7/9 01/09/00
Initiation au langage C.
Niveau 2.
sound (1000);
delay (100);
nosound ();
}
void saisie (int x, int y, char *chaine, int max_car)
{

char car = ' '; int nb_car = 0;

gotoxy (x,y);
car = getch ();
while ((car != 0x0D) && (nb_car < max_car))
{
switch (car)
{
case 0:
{
car = getch ();

/* nb_car = 0 : d‚but de la chaine */ if ((car == 75) && (nb_car>0))

{
x--;
nb_car--;

gotoxy (x,y); printf (" ");

/* printf avance le curseur de 1 position */
/* Réaffichage du curseur à la bonne position */ gotoxy (x,y);
}
else
{
beep ();
}
}
break;
case 8:
{
if (nb_car>0)
{
x--;
nb_car--;

gotoxy (x,y); printf (" ");

/* printf avance le curseur de 1 position */
/* Réaffichage du curseur à la bonne position */ gotoxy (x,y);
}
else
beep ();
}
break;
default:
{

Berthomier Eric Page 8/9 01/09/00
Initiation au langage C.
Niveau 2.

/* Ajout du caractère dans le tableau */ chaine [nb_car] = car;

/* Affichage à l'écran */
printf ("%c",chaine [nb_car]);

/* Mise à jour de la position dans le tableau */ nb_car ++;
/* Mise à jour de la position à l'écran */ x++;

gotoxy (x,y);
}
break;
/* Fin du switch case */
/* Saisie d'un nouveau caractère */
car = getch ();
}
/* Ajout du 0 binaire de fin de chaîne */
chaine [nb_car] = 0;
}
void main ()
{
char chaine [11];
char message [] = "Votre saisie : ";
clrscr ();
gotoxy (1,3);
printf ("%s", message);

/* 10 : Laisser de la place pour le 0 binaire de fin de chaîne */ saisie (strlen (message)+1,3,chaine,10);

/* Affichage du résultat */
gotoxy (1,5);
textcolor (GREEN);
cprintf ("Votre saisie : %s",chaine);
/* Attente pour visualisation */
getch ();
}

Berthomier Eric Page 9/9 01/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_2_3.rtf

Le graphisme (1ère partie)

Ras le bol de l'alphanumérique et du 25 * 80 de grand papa… Vive le VGA enfin c'est déjà démodé mais avec notre Turbo C 2.0 c'est tout ce que nous pouvons faire. A noter tout de même que la plupart des jeux d'action Doom Like ont été écrits en MCGA (300*200*256 couleurs !!!).

3.1 Mise en fonction du graphisme & utilisation de l'aide

3.1.1 Utilisation de l'aide

Nous allons apprendre à utiliser l'aide en ligne de Turbo C 2.0.
L'aide en ligne s'affiche lors de l'appui simultané de Ctrl et F1 sur un mot clé.
Exemple :
La fonction initgraph provoque l'affichage de : Ce que fait notre fonction.

Indique la bibliothèque nécessaire
pour l'utilisation de cette fonction.

Liens vers d'autres fonctions
Descriptif d'appel de la fonction qui s'utilise conjointement à la
nôtre.

Le préfixe far signifie une valeur longue du pointeur (peu importe). Pour notre connaissance générale, il faut simplement savoir que c'est comme si les pointeurs normaux ne pouvaient adresser du courrier que sur la France et les pointeurs far sur le Monde. Les pointeurs far ont disparu avec les compilateurs 32 bits.

Sinon, on retrouve tous les termes précédemment employés …

La bibliothèque nécessaire nous indique qu'il nous faudra ajouter : #include <graphics.h> à la liste.

Maintenant à nous de jouer.
3.1.2 Les fonctions à utiliser
Voici les fonctions que l'on va utiliser dans un premier temps :

initgraph (…)

Berthomier Eric Page 1/9 01/09/00
Initiation au langage C.
Niveau 2.

graphresult() closegraph ()

Exercice :
En utilisant l'aide en ligne, essayez de comprendre l'utilisation des 3 fonctions.
Explications :

Par défaut, les programmes écrits en C fonctionnent en mode Texte. La fonction initgraph permet de passer en mode graphique. Ce mode graphique demeure jusqu’à la rencontre de le fonction closegraph qui arrêtera le mode graphique pour repasser en mode Texte. Et la fonction graphresult alors ? Elle sert uniquement à savoir si le passage du mode texte au mode graphique s’est bien passée.

3.1.3 Ma fonction d'initialisation du graphisme

Mais où est ce que l'on trouve cela … Bon, je vais vous livrer la potion magique du programmeur … Ça ne va pas non ! Bon un morceau …

La première phase consiste à chercher des exemples.

Pour trouver l'utilisation des fonctions on utilise les aides livrées avec le logiciel de programmation ou les livres comme par exemple "Aide mémoire de

ou Internet.

Dans le cas présent, vous pouvez ouvrir le fichier Bgidemo.c qui se trouve dans le répertoire exemple et regarder. C’est bizarre il y a des ressemblances avec ce qui suit.
La seconde phase consiste à essayer de comprendre puis de faire des tests et enfin de se jeter à l’eau en passant au codage …

Et ça se passe toujours comme ça !

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du résultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));

Berthomier Eric Page 2/9 01/09/00
Initiation au langage C.
Niveau 2.

return ( 1 ); /* Problème */
}
}

int main ()
{

if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
getch ();

closegraph (); /* Fermeture du mode graphique */ return 0;

}
Exercice :
A l'aide de l'aide en ligne comprenez le programme (simple …).

3.1.4 Les fichiers BGI

Les fichiers BGI (*.BGI) sont des pilotes (non pas des pilotes de courses), mais des pilotes de carte vidéo qui permettent d'aller écrire dans la carte vidéo. Dans le turbo C vous trouverez différents pilotes. Par la fonction Windows et la commande rechercher, réaliser la recherche des fichiers *.BGI en partant du répertoire d'installation (c:\tc).

Je ne connais pas toutes les reliques d'écran et je ne veux pas connaître toutes les reliques d'écran. Je peux tout de même vous dire que :

Egavga.bgi sert à l'EGA et le VGA, c'est celui que nous utiliserons.
Herc.BGI est utilisée pour les écrans Hercule (noir et blanc).
Cga.bgi pour les écrans CGA.
3.2 Un fichier squelette

Ras le bol de retaper toujours le même corps de programme … Nous allons devoir faire beaucoup de petits programmes pour effectuer des tests sur les nouvelles fonctions. Je vous conseille donc de sauvegarder le précédent petit programme sous le nom squel.c (squelette) puis de le réutiliser pour chaque nouveau petit programme, en prenant soin de faire write as (sauvegarder sous) avec le nouveau nom de programme dès le début pour ne pas écraser squelet.c

Berthomier Eric Page 3/9 01/09/00
Initiation au langage C.
Niveau 2.
3.3 Structure d’un programme

Voilà notre squelette de programme écrit mais en général, sans mode graphique, un programme C s’écrit avec le squelette suivant :

Déclaration des bibliothèques
#include <stdio.h>
#include <conio.h>
#include "prog.h"
Déclaration des variables globales : elles sont à proscrire impérativement.
int pas_bien = 0 ;
Déclaration des fonctions
int toto ()
{
printf ("Bonjour, cette fonction ne fait pas grand chose !\n") ; }
Fonction principale
int main ()
{
toto () ;
}
3.4 Déclaration des bibliothèques
#include "… .h" indique que nous allons utiliser un fichier .h qui se trouve dans notre
répertoire.
#include <… .h> indique que nous allons utiliser un fichier .h qui se trouve dans les
librairies du C. Cet endroit est spécifié dans le menu Options Compiler.
Le sujet est vaste et je ne m’attarderai pas dessus.
3.5 Utilisation du graphisme

3.5.1 Ecriture de texte

La fonction à utiliser pour écrire du texte en mode graphique est outtextxy.
Par la fonction Ctrl-F1, j'obtiens son prototypage :
void far outtextxy (int x, int y, char far* textstring);
Exercices
Tester la fonction.
Exemple
J'ouvre mon squelette et je rajoute ce qui est en gras

int main ()
{

if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

Berthomier Eric Page 4/9 01/09/00
Initiation au langage C.
Niveau 2.
}
outtextxy (30,30, "Coucou c'est moi !");
getch ();
closegraph ();
return (0);
}
Regardez, comprenez …
Ecrivez en 20,50, Oh C mon beau C, que C beau.
3.5.2 Notations

Vous êtes arrivé maintenant à un stade auquel le C ne doit plus vous effrayer, je vous décrits donc les fonctions comme elles le sont dans l’aide en ligne. Je vous rappelle que :

void signifie indéfini. void à l’entrée d’une fonction signifie donc qu’elle ne
renverra rien.
far signifie loin. Une antiquité, faîtes comme si elle n’existait pas. 3.5.3 Couleur de tracé

La définition de la couleur de tracé est obtenue par la fonction :
void far setcolor (int color);
3.5.4 Lignes, cercles et rectangles
Les fonctions respectives pour dessiner une ligne, un cercle ou un carré sont :
void far line (int x1, int y1, int x2, int y2);
void far circle (int x, int y, int radius);
void far rectangle (int left, int top, int right, int bottom);
Exercice :
Tester ces 3 fonctions dans un même programme.
3.5.5 Effacement de l'écran
Arghhh !!! Notre vielle fonction clrscr () ne marche plus … Pour effacer l'écran il faut maintenant utiliser la fonction graphique :
void far cleardevice (void);
3.6 Exercice complet et instructif …

Ecrire un programme qui propose les 6 choix suivants à l'utilisateur :
Dessiner une ligne
Dessiner un rectangle
Dessiner un cercle
Choisir la couleur
Sortir
qui saisisse le choix de l'utilisateur et fait afficher ce qui est écrit (dans les cas 1,2,3).

Berthomier Eric Page 5/9 01/09/00
Initiation au langage C.
Niveau 2.
Correction des exercices du chapitre 3
Exercice 3.4.1

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du r‚sultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{

if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
outtextxy (20,50, "Oh C mon beau C, que C beau");
getch ();
closegraph (); /* Fermeture du mode graphique */
return 0;
}
Exercice 3.4.3

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

Berthomier Eric Page 6/9 01/09/00
Initiation au langage C.
Niveau 2.

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du résultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{

if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
setcolor (7);
line (10,10,250,170);
setcolor (3);
circle (250,350,100);
setcolor (4);
rectangle (100,200,350,400);
getch ();
closegraph (); /* Fermeture du mode graphique */
return 0;
}
Exercice 3.5

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du r‚sultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));

Berthomier Eric Page 7/9 01/09/00
Initiation au langage C.
Niveau 2.

return ( 1 ); /* Problème */
}
}
int main ()
{
char car = ' ';

int couleur = 1; /* On évite le 0 car noir sur fond noir !!!! */ int sortie = 0;
if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
do
{
/* Affichage du menu */
cleardevice ();
setcolor (couleur);
outtextxy (0,0,"1. Dessiner une ligne.");
outtextxy (0,10,"2. Dessiner un rectangle.");

outtextxy (0,20,"3. Dessiner un cercle."); outtextxy (0,30,"4. Changer la couleur.");

outtextxy (0,50,"9. Sortir.");
car = getch ();
switch (car)
{
case '1':
cleardevice ();
line (100,100,250,370);

outtextxy (200,400, "Appuyez sur une touche."); getch ();

break;
case '2':
cleardevice ();
rectangle (100,200,350,400);

outtextxy (200,400, "Appuyez sur une touche."); getch ();

break;
case '3':
cleardevice ();
circle (150,250,100);

outtextxy (200,400, "Appuyez sur une touche."); getch ();

break;
case '4':
couleur ++;
if (couleur > 15)
couleur = 1;
break;
case '9':
sortie = 1;
break;

Berthomier Eric Page 8/9 01/09/00
Initiation au langage C.
Niveau 2.
}
}
while (!sortie);
closegraph (); /* Fermeture du mode graphique */
return 0;
}

Berthomier Eric Page 9/9 01/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_2_4.rtf

Le graphisme (2nde partie)

4.1 Valeur de retour de la fonction main ()
La fonction main peut être déclarée avec une valeur de retour :
int main ()
ou sans:
void main ()
Le choix entre ces deux façons réside dans l’utilisation de votre programme :
le fait de renvoyer une valeur (un code erreur) permet lors de l’exécution de votre programme par un autre programme de savoir si tout s’est bien passé.
le fait de ne pas renvoyer de code erreur rend impossible le fait de savoir si le programme s’est bien terminé (sauf de manière visuelle (en l’exécutant)).

On utilisera généralement void main () pour des programmes de test et int main () pour des vrais programmes.

4.2 Motif de remplissage et contour

Dans tous les exemples qui suivront, j'utiliserai la fonction Initialize () écrite dans le précédent cours.

4.2.1 Exemple

Tapez cet exemple et en vous appuyant sur l’aide en ligne, comprenez l’utilisation des fonctions graphiques setlinestyle, setfillstyle, fillellipse.

#include<stdio.h>
#include<conio.h>
#include<stdlib.h>
#include<dos.h>
#include<graphics.h>
int main ()
{
clrscr();
/* Ouverture du mode graphique */
if (Initialize ())
return (1);
/* Définit la couleur de tracé des traits */
setcolor (15);

/* Définit l'épaisseur et le style courants des lignes */ setlinestyle (DOTTED_LINE, 0, NORM_WIDTH);

/* Définit le motif et la couleur de remplissage */
setfillstyle (SOLID_FILL, 12);

/* Dessine une ellipse de rayon x=15 y=15 donc un cercle rayon 15 */ fillellipse (30,30,15,15);

/* Attend l'appui d'une touche : permet de voir ce que l'on fait */

Berthomier Eric Page 1/10 06/09/00
Initiation au langage C.
Niveau 2.
getch ();
/* Termine le mode graphique */
closegraph();
}

4.2.2 Exercices d'application

Changer la couleur du contour
Changer le motif de remplissage.
Changer la couleur du motif de remplissage
4.3 Rectangles et barres

4.3.1 Exemple

#include<stdio.h> #include<conio.h>

#include<stdlib.h>
#include<dos.h>
#include<graphics.h>
int main ()
{
clrscr();
/* Ouverture du mode graphique */
if (Initialize ())
return (1);
/* Définit la couleur de tracé des traits */
setcolor (13);
/* Définit le motif et la couleur de remplissage */
setfillstyle (SOLID_FILL, 11);
/* Barre 3D */
bar3d (50,50,200,200,13,1);

/* Attend l'appui d'une touche : permet de voir ce que l'on fait */ getch ();

closegraph();
}

4.3.2 Explications

void far bar3d (int left, int top, int right, int bottom, int depth, int topflag);
L'aide de bar3d n'est pas explicite, en voici donc une explication plus complète
(extrait du Manuel de Référence) :
bar3d dessine une barre rectangulaire en trois dimensions (dans l'espace), et la remplit

l'aide du motif et de la couleur de remplissage courants. Le contour dans l'espace de cette barre est tracé dans le style et avec la couleur de tracé courants. La profondeur de la barre est donnée en pixels par l'argument depth. L'argument topflag sert à

Berthomier Eric Page 2/10 06/09/00
Initiation au langage C.
Niveau 2.

déterminer si la barre possède une face supérieure. Si topflag est non nul il y a une face supérieure sinon il n'y en a pas (ce qui permet l'empilage de plusieurs barres). Pour la profondeur de la barre, prendre par exemple 25 % de la barre.

On reconnaît dans topflag le fameux type vrai faux (vrai si différent de 0, faux sinon).
4.3.3 Exercices d'application
Changer la couleur de trait
Dessiner un rectangle plein

4.4 Texte graphique : oh les belles lettres …

4.4.1 Exemple

#include<stdio.h> #include<conio.h>

#include<stdlib.h>
#include<dos.h>
#include<graphics.h>
int main ()
{
clrscr();
/* Ouverture du mode graphique */
if (Initialize ())
return (1);
/* Définit la couleur du texte */
setcolor (10);
/* Définit le style du texte */
settextstyle (TRIPLEX_FONT,VERT_DIR,5);
/* Affiche le texte */
outtextxy (30,30,"Raven come back");

/* Attend l'appui d'une touche : permet de voir ce que l'on fait */ getch ();

/* Ferme le mode graphique */
closegraph();
}
4.4.2 Taille du texte
Pour connaître la taille des caractères utilisés, nous devons utiliser la fonction :
void gettextsettings (struct textsettingstype far* texttypeinfo) ;
Aïe, quelque chose que je ne connais pas …

Le mot de syntaxe struct nécessiterait un cours à lui tout seul. Nous allons donc utiliser une autre méthode pour connaître la taille des caractères. Relisez l’aide en ligne associée à

settextstyle et plus spécifiquement l’aide associée au paramètre charsize. Eh oui, nous avons

Berthomier Eric Page 3/10 06/09/00
Initiation au langage C.
Niveau 2.

la clé de notre problème de taille de caractères, la taille en pixels des caractères est égal à charsize * 8.
Dans l’exemple précédent où charsize était de 5 nous avons donc des caractères de 40x40 pixels.

4.4.3 Exercices d'application
Changer le texte …
Changer la couleur du texte
4.5 Exercice complet et instructif …
A l'aide de l'exercice 3.4 du cours n°3, réaliser un menu en mode graphique.
Spécifications :
Dessiner le menu en mode graphique et en caractères gothiques.
Ajoutez un menu texte horizontal qui affiche un texte en horizontal en
caractères normaux (DEFAULT_FONT).
Ajoutez un menu texte horizontal qui affiche un texte en vertical en
caractères normaux (DEFAULT_FONT).

Berthomier Eric Page 4/10 06/09/00
Initiation au langage C.
Niveau 2.
Correction des exercices du chapitre 4

Exercice 4.2.2
Changer la couleur du contour
Changer le motif de remplissage.
Changer la couleur du motif de remplissage

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du résultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{
char car = ' ';

int couleur = 1; /* On évite le 0 car noir sur fond noir !!!! */ int sortie = 0;
if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
/* Couleur du contour */
setcolor (8);
/* Définition du style de ligne */
setlinestyle (4, 0, NORM_WIDTH);
/* Définition du type et couleur de remplissage */
setfillstyle (SLASH_FILL, 2);
/* Ellipse pleine */
fillellipse (30,30,15,15);
/* Attente */

Berthomier Eric Page 5/10 06/09/00
Initiation au langage C.
Niveau 2.
getch ();
closegraph (); /* Fermeture du mode graphique */
return 0;
}
Exercice 4.3.3
Changer la couleur de trait
Dessiner un rectangle plein

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du r‚sultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{
char car = ' ';

int couleur = 1; /* On évite le 0 car noir sur fond noir !!!! */ int sortie = 0;
if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
/* Couleur du contour */
setcolor (4);
/* D‚finition du type et couleur de remplissage */
setfillstyle (SLASH_FILL, 4);
/* Ellipse pleine */
bar3d (50,50,200,200,0,1);
/* Attente */
getch ();
closegraph (); /* Fermeture du mode graphique */
return 0;
}

Berthomier Eric Page 6/10 06/09/00
Initiation au langage C.
Niveau 2.
Exercice 4.4.3
Changer le texte …
Changer la couleur du texte

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du résultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{
char car = ' ';

int couleur = 1; /* On évite le 0 car noir sur fond noir !!!! */ int sortie = 0;
if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
/* Couleur du contour */
setcolor (4);
/* Définition du type et couleur de remplissage */
settextstyle (TRIPLEX_FONT, VERT_DIR, 5);
/* Affichage du texte */
outtextxy (30,30, "Coucou");
/* Attente */
getch ();
/* Fermeture du mode graphique */
closegraph ();
return 0;
}

Berthomier Eric Page 7/10 06/09/00
Initiation au langage C.
Niveau 2.
Exercice 4.5

Dessiner le menu en mode graphique et en caractères gothiques.

Ajoutez un menu texte horizontal qui affiche un texte horizontal en caractères normaux. Ajoutez un menu texte horizontal qui affiche un texte vertical en caractères normaux.

#include <graphics.h>

#include <conio.h> #include <stdio.h>

#include <stdlib.h>
int Initialize(void)
{

int GraphDriver = DETECT; /* Driver de carte graphique */ int ErrorCode; /* Rapport des codes erreurs */ int GraphMode=0;

initgraph( &GraphDriver, &GraphMode, "c:\\tc" );
ErrorCode = graphresult();
/* Lecture du r‚sultat de l'initialisation */
if( ErrorCode != grOk )
{
printf("Erreur de système graphique : %s\n",
grapherrormsg(ErrorCode ));
return ( 1 ); /* Problème */
}
}
int main ()
{
char car = ' ';

int couleur = 1; /* On évite le 0 car noir sur fond noir !!!! */ int sortie = 0;
if (Initialize ()) /* Ouverture du mode graphique */ {
printf ("Impossible de passer en mode graphique"); return (1);

}
/* Choix de la police de caractère */
settextstyle (GOTHIC_FONT, HORIZ_DIR, 3);
/*
La taille des caractères est maintenant de 3*8 = 24 pixels Afin de laisser de la place pour mon rectangle j'‚cris donc une ligne tous les 30 pixels et … 10 pixels du bord de l'‚cran
*/
do
{
/* Affichage du menu */
cleardevice ();
setcolor (couleur);
outtextxy (10,30,"1. Dessiner une ligne.");
outtextxy (10,60,"2. Dessiner un rectangle.");
outtextxy (10,90,"3. Dessiner un cercle.");
outtextxy (10,120,"4. Changer la couleur.");

Berthomier Eric Page 8/10 06/09/00
Initiation au langage C.
Niveau 2.
outtextxy (10,150,"5. Texte horizontal.");
outtextxy (10,180,"6. Texte vertical.");
outtextxy (10,250,"9. Sortir.");
car = getch ();
switch (car)
{
case '1':
cleardevice ();
line (100,100,250,370);

outtextxy (200,400,"Appuyez sur une touche"); getch ();

break;
case '2':
cleardevice ();
rectangle (100,200,350,400);

outtextxy (200,400,"Appuyez sur une touche"); getch ();

break;
case '3':
cleardevice ();
circle (150,250,100);

outtextxy (200,400,"Appuyez sur une touche"); getch ();

break;
case '4':
couleur ++;
if (couleur > 15)
couleur = 1;
break;
case '5':
cleardevice ();
/* On passe en mode normal */
settextstyle (DEFAULT_FONT, HORIZ_DIR, 1);

outtextxy (100,100, "Ceci est un texte"); /* On revient au style gothique */

settextstyle (GOTHIC_FONT, HORIZ_DIR, 3);

outtextxy (200,400, "Appuyez sur une touche"); getch ();

break;
case '6':
cleardevice ();
/* On passe en mode normal */
settextstyle (DEFAULT_FONT, VERT_DIR, 1);

outtextxy (100,100, "Ceci est un texte"); /* On revient au style gothique */

settextstyle (GOTHIC_FONT, HORIZ_DIR, 3);

outtextxy (200,400, "Appuyez sur une touche"); getch ();

break;

Berthomier Eric Page 9/10 06/09/00
Initiation au langage C.
Niveau 2.

case '9':
sortie = 1;
break;
}
}
while (!sortie);
closegraph (); /* Fermeture du mode graphique */
return 0;
}

Berthomier Eric Page 10/10 06/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Initiation_au_C_niveau_2_5.rtf

Fichiers et Structures

5.1 Bases sur les fichiers

Un fichier représente tout ce qui est enregistré sur votre disque dur ou presque, on va dire tout ce qui porte un nom. Il est possible de créer, de lire ou d’écrire dans des fichiers. Il faut noter cependant que certains fichiers par contre peuvent être protégés en lecture, en écriture ou les deux.
Afin de ne rien endommager dans notre système, créez à l’aide de NotePad ou de la commande Ms-Dos « edit » un fichier nommé test.txt, dans lequel vous taperez un texte de louanges pour ce superbe cours (à votre libre inspiration).

Voici un petit exemple de la lecture du fichier test.txt.
Exemple
#include <conio.h>
#include <io.h>
#include <fcntl.h>
int main ()
{
int h_fic;

char ligne [80]; int nb_car_lus;

int i;
/* effacement de l'écran */
clrscr ();
/* Ouverture du fichier */
h_fic = open ("c:\\test.txt ", O_CREAT);
/* Test si fichier ouvert */
if (h_fic == -1)
{
printf ("Impossible d'ouvrir le fichier");
getch ();
return (1);
}
while (!eof (h_fic))
{
/* Lecture de 80 octets maximum */
nb_car_lus = read (h_fic, ligne, 80);
/* Ecriture de ce qui a ‚t‚ lu */
for (i=0; i<nb_car_lus; i++)
{
printf ("%c",ligne [i]);
}
printf ("\n");
}
/* Fermeture du fichier */
close (h_fic);

Berthomier Eric Page 1/8 06/09/00
Initiation au langage C.
Niveau 2.

/* Ecrire que c'est terminé */
printf ("\n --- Fin ---");
getch ();
return (0);
}
Analysons l’ensemble
/* Ouverture du fichier */
h_fic = open ("c:\\test.txt", O_CREAT);
La fonction open permet d'ouvrir un fichier, le second paramètre est son mode

d'ouverture, ici O_CREAT signifiant ouverture s'il existe ou création s'il n'existe pas. Par contre O_CREAT seul ne permet pas d'écrire dans le fichier offrant ainsi une sécurité supplémentaire. La fonction open renvoie –1 si l'on a pas réussi à ouvrir notre fichier et renvoie un handle (poignée) sur le fichier sinon.

What is an handle ?
Un lien sur quelque chose. On appelle ceci une poignée car comme avec un sac, vous le prenez par la poignée pour le porter.
Notez la présence de \\ au lieu de \ du fait que \ seul permet la composition de caractère spéciaux (ex \n).
/* Test si fichier ouvert */
if (h_fic == -1)
{
printf ("Impossible d'ouvrir le fichier");
getch ();
return (1);
}
La fonction eof permet de savoir si l'on a atteint la fin du fichier. Regardez l'aide pour
comprendre le while (!eof (…))
while (!eof (h_fic))
{
La fonction read permet de lire des octets (ici transformés en char). Elle prend pour

paramètre le handle de fichier, la zone de mémoire dans laquelle on va recevoir les données et la taille maximale que l'on veut lire (attention au dépassement (cf. chapitre 2)).

/* Lecture de 80 octets maximum */
nb_car_lus = read (h_fic, ligne, 80);
/* Ecriture de ce qui a été lu */
for (i=0; i<nb_car_lus; i++)
{

Berthomier Eric Page 2/8 06/09/00
Initiation au langage C.
Niveau 2.
printf ("%c",ligne [i]);
}
printf ("\n");
}

A ne pas oublier, il faut fermer le fichier après l'avoir ouvert !
/* Fermeture du fichier */
close (h_fic);
/* Ecrire que c'est terminé */
printf ("\n --- Fin ---");
getch ();
return (0);
}
Très simple non !
5.2 Création d'un autre fichier
Reprenez l'exemple précédent et ajouter ceci après le open :
h_fic2= open ("c:\\ma_copie.bat", O_CREAT);
N'oubliez pas de déclarer h_fic2.
Ainsi nous allons créer une copie du fichier.
int h_fic2;
/* Création d'une copie */
h_fic2 = open ("c:\\ma_copie.txt", O_CREAT);
/* Test si fichier bien créer */
if (h_fic2 == -1)
{

printf ("Impossible de créer le nouveau fichier"); close (h_fic2);

getch ();
return (1);
}
while (!eof (h_fic))
{
/* Coupure */
printf ("\n");
/* Ecriture dans notre copie de fichier */
write (h_fic2, ligne, nb_car_lus);
}
/* Fermeture du fichier */
close (h_fic);
/* Fermeture de ma copie de fichier */
close (h_fic2);

Berthomier Eric Page 3/8 06/09/00
Initiation au langage C.
Niveau 2.
En gras sont signalés tous les ajouts effectués.
La fonction write permet d'écrire des octets sur le fichier. Les paramètres sont le handle de
fichiers, les octets à écrire et la taille à écrire.

Essai, laissez 80 au lieu de nb_car_lus. Regardez, avec un peu de chance, vous allez trouver des morceaux de texte non désirés, eh oui, personne ne vous a dit que votre chaîne de départ était vide !

5.3 Ecriture dans un fichier : complément.
Insérer cette ligne après notre write :
/* Ecriture dans notre copie de fichier */
write (h_fic2, ligne, nb_car_lus);
oui la même ligne.
Regardez.
La fonction write a ajouté à la fin du fichier.

Attention, l'utilisation de O_CREAT permet de créer le fichier en mode lecture seul, pour l’ouvrir en plus avec des droits d’écriture il faut lui associer O_RDWR (lecture/écriture) ou O_WRONLY (écriture seule) de la façon suivante :

open ("c:\\test.txt", O_CREAT | O_RDWR).
5.4 Positionnement et taille d'un fichier

La taille d'un fichier est donnée par la fonction int filelength (handle)
La position en octets dans un fichier est donnée par la fonction int tell (handle)
Exemple

#include <conio.h>
#include <io.h>
#include <fcntl.h>
int main ()
{
int position;
int h_fic;
char ligne [80];
int nb_car_lus;
int i;
int taille;
/* effacement de l'écran */
clrscr ();
/* Ouverture du fichier */
h_fic = open ("c:\\test.txt", O_CREAT);
/* Test si fichier ouvert */
if (h_fic == -1)
{
printf ("Impossible d'ouvrir le fichier");

Berthomier Eric Page 4/8 06/09/00
Initiation au langage C.
Niveau 2.
getch ();
return (1);
}
/* Taille du fichier */
taille = filelength (h_fic);
printf ("Taille du fichier : %d\n",taille);
while (!eof (h_fic))
{
/* Position dans le fichier */
position = tell (h_fic);

printf ("Position dans le fichier : %d\n",position); /* Lecture de 80 octets maximum */

nb_car_lus = read (h_fic, ligne, 80);
/* Ecriture de ce qui a ‚t‚ lu */
for (i=0; i<nb_car_lus; i++)
{
printf ("%c",ligne [i]);
}
printf ("\n");
}
/* Position dans le fichier */
position = tell (h_fic);
printf ("Position dans le fichier : %d\n",position);
/* Fermeture du fichier */
close (h_fic);
printf ("\n --- Fin ---");
getch ();
return (0);
}
5.5 Structures

Nous avons vu, dans le chapitre précédent, l'utilisation des structures. Nous allons maintenant voir la définition et l'utilisation d'une structure.

5.5.1 Déclaration

choix se proposent à nous.
Solution 1 :
struct
{
/* Définition de la structure */
nom de la variable qui aura comme forme cette structure;
Solution 2 :
typedef struct
{
/* Définition de la structure */
nom de la structure;

Berthomier Eric Page 5/8 06/09/00
Initiation au langage C.
Niveau 2.

<nom de la structure> nom de la variable qui aura comme structure cette structure. Je préfère de loin la seconde solution.
Exemple
typedef struct
{
char nom [40];
char prenom [20];
int age;
elt_fiche_nom;
elt_fiche_nom une_fiche ;
elt_fiche_nom une_seconde_fiche ;

elt_fiche_nom est le nom de la stucture, la macro typdef signifie définition d'un type, de ce fait là elt_fiche_nom se comporte exactement comme un autre type de données (int par exemple).

Par la suite, je n'utiliserai que la seconde méthode.
5.5.2 Utilisation

struct
{
char nom [40];
char prenom [20];
int age;
fiche_nom;
fiche_nom une_fiche;
fiche_nom une_seconde_fiche;
une_fiche.age = 20;

5.5.3 Taille d'une structure
La taille d'une structure ou d'un type se détermine par la fonction sizeof .
Exemple
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
#include <fcntl.h>
int main()
{
typedef struct
{
char nom [40];
char prenom [20];

Berthomier Eric Page 6/8 06/09/00
Initiation au langage C.
Niveau 2.
int age;
fiche;
fichema_fiche;

printf ("Taille de la structure : %d\n",sizeof (fiche)); sprintf (ma_fiche.nom, "%s","BERTHOMIER");

sprintf (ma_fiche.prenom, "%s","Eric");
ma_fiche.age = 30;
printf ("%s %s est âgé de %d",ma_fiche.prenom, ma_fiche.nom, ma_fiche.age);
getch ();
return (0) ;
}

5.6 Fichier & structure

Ce programme ne fonctionne pas sous win98 avec le compilateur Borland C 2. (Compilateur 16 bits) Il fonctionne par contre avec un compilateur 32 bits (Test avec VC++ 6.0).
L’explication existe mais est hors de portée de ce cours ou alors un bug de ma part. Peut être, peut être. Nul n’est parfait.

Je laisse tout de même celui-ci à titre d’exemple.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <io.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
typedef struct
{
char nom [40];
char prenom [20];
unsigned char age;
fiche;
fichema_fiche;
int h_fic ;
int i;
/* Création du du fichier -> le vider s'il existait */

h_fic = open ("c:\\fiche.dat", O_CREAT | O_TRUNC | O_WRONLY); /* Test si fichier ouvert */

if (h_fic == -1)
{
printf ("Impossible d'ouvrir le fichier");
getch ();
return (1);
}

printf ("Taille de la structure : %d\n\n",sizeof (fiche)); sprintf (ma_fiche.nom, "%s","BERTHOMIER");

sprintf (ma_fiche.prenom, "%s","Eric");
ma_fiche.age = 30;

Berthomier Eric Page 7/8 06/09/00
Initiation au langage C.
Niveau 2.

printf ("Fiche 1 : %s %s est âgé de %d\n",ma_fiche.prenom, ma_fiche.nom, ma_fiche.age);

write (h_fic, &ma_fiche, sizeof (fiche));
sprintf (ma_fiche.nom, "%s","P'TIT");
sprintf (ma_fiche.prenom, "%s","Luc");
ma_fiche.age = 48;

printf ("Fiche 2 : %s %s est âgé de %d\n",ma_fiche.prenom, ma_fiche.nom, ma_fiche.age);

write (h_fic, &ma_fiche, sizeof (fiche));
sprintf (ma_fiche.nom, "%s","Tolkien");
sprintf (ma_fiche.prenom, "%s","JRR");
ma_fiche.age = 89;

printf ("Fiche 3 : %s %s est âgé de %d\n",ma_fiche.prenom, ma_fiche.nom, ma_fiche.age);

write (h_fic, &ma_fiche, sizeof (fiche));
/* Fermeture du fichier */
close (h_fic);

/* --------------------------------- */ /* Lecture des éléments du fichier */ /* --------------------------------- */

/* Ouverture du fichier */
h_fic = open ("c:\\fiche.dat", O_RDONLY);
/* Test si fichier bien ouvert */
if (h_fic == -1)
{
printf ("Impossible d'ouvrir le fichier");
getch ();
return (1);
}
for (i=0; i<3; i++)
{
/* Lecture de la fiche */
read (h_fic, &ma_fiche, sizeof (fiche));
/* Affichage des donn‚es lues */

printf ("Fiche n° %d : %s %s est âgé de %d\n", i, ma_fiche.prenom, ma_fiche.nom, ma_fiche.age);

}
/* Fermeture du fichier */
close (h_fic);

getch (); return 0;

}
5.7 Fin et commencement

Vous disposez maintenant d'à peu près toutes les connaissances nécessaires pour l'élaboration de petits programmes. D’autres cours suivront, en fonction de la demande.

Pour tout commentaire, suggestion de cours, écrivez moi :
eric.berthomier@free.fr ou lesouriciergris@free.fr

Berthomier Eric Page 8/8 06/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\TP1.rtf

Jeu des allumettes
Adaptée du livre de Jacques Deconchat : "102 Programmes Pour MO6, TO8 et TO9+"

Un certain nombre d’allumettes est disposé entre les deux partis, l’ordinateur et vous. Le but du jeu est de ne pas retirer la dernière allumette. Pour se faire, une prise maximale est désignée par le joueur.

En début de partie, on demande :
Le nombre d’allumettes disposé entre les deux joueurs (de 3 à 100).
Le nombre maximal d’allumettes que l’on peut retirer.
Qui commence (0 pour le joueur et 1 pour l’ordinateur).

Puis, tour à tour, chaque parti donne le nombre d’allumettes qu’il prend. L’ordinateur répond grâce à la fonction décrite ci-dessous :

int jeu_ordi (int nb_allum, int prise_max)
{
int prise = 0;
int s = 0;
float t = 0;
s = prise_max + 1;
t = ((float) (nb_allum - s)) / (prise_max + 1);
while (t != floor (t))
{
s--;
t = ((float) (nb_allum-s)) / (prise_max + 1);
}
prise = s - 1;
if (prise == 0)
prise = 1;
return (prise);
}
La fonction floor donne l’arrondi inférieur d’un nombre.

Nb_allum est le nombre d’allumettes sur la table (au moment du coup à jouer). Prise_max est le nombre maximum d’allumettes autorisé lors d’une prise.

La fonction retourne en sortie le nombre d’allumettes prises par l’ordinateur.
Exemple :
prise = jeu_ordi (10,3) ;

La partie se termine lorsqu’il n’y a plus d’allumettes sur la table. La personne ayant tiré la dernière allumette est le perdant, l’autre le vainqueur.

Berthomier Eric Page 1/3 06/09/00
Initiation au langage C.
Travaux Pratiques.
Exemple de solution

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int jeu_ordi (int nb_allum, int prise_max)
{
int prise = 0;
int s = 0;
float t = 0;
s = prise_max + 1;
t = ((float) (nb_allum - s)) / (prise_max + 1);
while (t != floor (t))
{
s--;
t = ((float) (nb_allum-s)) / (prise_max + 1);
}
prise = s - 1;
if (prise == 0)
prise = 1;
return (prise);
}
int main ()
{
int nb_max_d=0; /*nbre d'allumettes maxi au d‚part*/ int nb_allu_max=0; /*nbre d'allumettes maxi que l'on peut tirer au maxi*/
int qui=0; /*qui joue? 0=Nous --- 1=PC*/ int prise=0; /*nbre d'allumettes prises par le joueur*/ int nb_allu_rest=0; /*nbre d'allumettes restantes*/
clrscr ();
printf ("Nombre d'allumettes disposées entre les deux joueurs (entre
et 300): \n");
scanf ("%d",&nb_max_d);
do
{
printf ("\nNombre maximal d'allumettes que l'on peut retirer: \n");
scanf ("%d",&nb_allu_max);
}
while (nb_allu_max >= nb_max_d);
/* On répète la demande de prise tant que le nombre donné n'est pas correct */

printf ("\nQuel joueur commence? 0--> Joueur ; 1--> Ordinateur: \n"); scanf ("%d",&qui);

nb_allu_rest = nb_max_d;
do
{

Berthomier Eric Page 2/3 06/09/00
Initiation au langage C.
Travaux Pratiques.

printf ("\nNombre d'allumettes restantes : %d",nb_allu_rest); if (qui==0)

{
do
{

printf ("\nCombien d'allumettes souhaitez-vous piocher?\n");

scanf ("%d",&prise);
}
while ((prise > nb_allu_rest) || (prise > nb_allu_max)); /* On répète la demande de prise tant que le nombre donné n'est pas correct */
}
else
{

prise = jeu_ordi (nb_allu_rest , nb_allu_max); printf ("\nPrise de l'ordi : %d\n",prise);

}
qui=!qui;
nb_allu_rest= nb_allu_rest - prise;
}
while (nb_allu_rest >0);
if (qui == 0) /* C'est à nous de jouer */
{
printf ("\nVous avez gagné!\n");
}
else
{
printf ("\nVous avez perdu!\n");
}
getch ();
return (0);
}

Berthomier Eric Page 3/3 06/09/00


D:\COM_CHM\Documentation CHM\_C C++\Cours C petit\Initiation_au_C\Tp2.rtf

Bataille navale simplifiée
 Adaptée du livre de Jacques Deconchat : "102 Programmes Pour MO6, TO8 et TO9+

Le jeu

Nous présentons ici un jeu simplifié de la bataille navale. Celui-ci ne comportera qu'un seul navire et le tableau des cases dans lequel peut se déplacer le navire sera limité à 10 cases sur 10 cases.
A chaque coup on devra donner les coordonnées en donnant l'horizontal (H) et la verticale (V). L'ordinateur répondra si le navire est coulé ou dans le cas contraire donnera la distance qui nous sépare du navire.

En fin de partie, on donnera le nombre de coups nécessaires à la destruction du navire.

Aide pour la programmation

Soit :
d : la distance (int) qui sépare le tir du navire.
x : la position horizontale du navire.
y : la position verticale du navire.
posx_bat : la position horizontale du tir.
posy_bat : la position verticale du tir.
La distance qui sépare le navire du tir est calculé par la formule suivante :
d =(int) (sqrt ((x- posx_bat)*(x- posx_bat)+(y- posy_bat)*(y- posy_bat)))
N'oubliez pas d'inclure la bibliothèque ‹math.h›

Complément d'informations pour la programmation

En mode texte on dessinera un tableau avec des numéros de 0 à 9 en hauteur et longueur. Les cases en elles-mêmes seront symbolisées par des points.
Dans un premier temps, on indiquera la solution afin de pouvoir contrôler le bon fonctionnement de notre programme.
Dans un second temps, la solution ne sera pas plus donnée et les coups précédemment tirés seront signifiés par des * au lieu du . .
La fonction atoi permet de transformer une chaîne de caractère en nombre. Pour connaître sa syntaxe consulter l’aide.

Berthomier Eric Page 1/3 06/09/00
Initiation au langage C.
Travaux Pratiques.
Exemple de solution

#include <conio.h>
#include <math.h>
#include <stdlib.h>
/* Dessine le plateau de jeu */
void plateau ()
{
int i=0,j=0;
clrscr ();
gotoxy (3,1);
for (i=0; i<10; i++)
printf ("%d ",i);
for (i=0; i<10; i++)
{
gotoxy (1,2+i);
printf ("%d ",i);
for (j=0; j<10; j++) printf (". ");

}
gotoxy (1,12);
printf ("Ligne : ");
gotoxy (11,12);
printf ("Colonne : ");
gotoxy (1,13);
printf ("Distance : ");
}
/* Saisie un chiffre en position x y */
int saisie_chiffre (int x, int y)
{
char car;
char chaine [2];
int sortie = 1; /* Ok on peut sortir */
do
{
/* On efface la précédente case */
gotoxy (x,y);
printf (" ");
/* On se repositionne pour la saisie */
gotoxy (x,y);
car = getch ();
if ((car<'0') || (car>'9'))
{
/* Saisie incorrect : beep */
sound (1000);

delay (10); nosound ();

/* On doit recommencer */
sortie = 0;
}
else
sortie = 1;
}

Berthomier Eric Page 2/3 06/09/00
Initiation au langage C.
Travaux Pratiques.
while (!sortie);
/* Affichage du caractère saisi */
gotoxy (x,y);
printf ("%c",car);

/* atoi transforme une chaîne de caractère en nombre */ /* transformation de notre caractère en chaîne */

chaine [0] = car;
chaine [1] = 0;
return (atoi (chaine));
}
void main ()
{
int posx_bat=0, posy_bat=0; /* Position du bateau */
int x,y; /* Position du tir */
int nb_coups=0; /* Nombre de coups */
int d=0; /* Distance */
randomize ();
posx_bat = random (10); /* 0 … 9 */
posy_bat = random (10);
plateau ();
do
{
/* Saisie de la colonne */

x=saisie_chiffre (9,12); /* Saisie de la ligne */

y=saisie_chiffre (21,12);
/* Nombre de coups */
nb_coups ++;
/* Affichage du coup */
gotoxy ((x+1)*2+1, y+2);
printf ("*");
/* Effacement des coordonnées */
gotoxy (9,12);
printf (" ");
gotoxy (21,12);
printf (" ");
/* Calcul de la distance */
d = (int) (sqrt (((x-posx_bat)*(x-posx_bat))+((y-posy_bat)*(y- posy_bat))));
/* Affichage de la distance */
gotoxy (12,13);
printf ("%d",d);
}
while (!((x==posx_bat) && (y==posy_bat)));
/* On recommence tant que la position saisie n'est pas celle du navire */ gotoxy (1,15);
printf ("Nombre de coups : %d",nb_coups);
getch ();
}

Berthomier Eric Page 3/3 06/09/00


Notes sur le langage C

  • Accueil Actualités IT Pro
  • ALM Cycle de vie du logiciel
    • ALM
    • Merise
    • UML
  • Java Plateforme et langage Java
    • Java
    • Java Web
    • Spring
    • Android
    • Eclipse
    • NetBeans
  • .NET Microsoft Framework .NET
    • Microsoft DotNET
    • Visual Studio
    • ASP.NET
    • C#
    • VB.NET
    • Windows Phone
    • Microsoft Azure
  • Dév. Web Développement Web et Webmarketing
    • Développement Web
    • AJAX
    • Apache
    • ASP
    • CSS
    • Dart
    • Flash / Flex
    • JavaScript
    • PHP
    • Ruby & Rails
    • Web sémantique
    • Webmarketing
    • (X)HTML
  • EDI Environnements de Développement Intégré
    • EDI
    • 4D
    • Delphi
    • Eclipse
    • LabVIEW
    • NetBeans
    • MATLAB
    • Scilab
    • Visual Studio
    • WinDev
    • Visual Basic 6
    • Lazarus
    • Qt Creator
  • Programmation Programmation et langages de programmation applicatifs
    • Programmation
    • Débuter - Algorithmique
    • 2D - 3D - Jeux
    • Assembleur
    • C
    • C++
    • Go
    • Objective C
    • Pascal
    • Perl
    • Python
    • Qt
    • XML
    • Autres
  • SGBD Systèmes de Gestion de Bases de Données
    • SGBD & SQL
    • 4D
    • Access
    • Big Data
    • DB2
    • Firebird
    • InterBase
    • MySQL
    • NoSQL
    • PostgreSQL
    • Oracle
    • Sybase
    • SQL-Server
  • Office Bureautique pour l'entreprise
    • Microsoft Office
    • Access
    • Excel
    • Word
    • Outlook
    • PowerPoint
    • SharePoint
    • Microsoft Project
    • OpenOffice & LibreOffice
  • Solutions d'entreprise Autres logiciels pour l'entreprise
    • Solutions d'entreprise
    • Big Data
    • Business Intelligence
    • ERP / PGI
    • CRM
    • SAS
    • Cloud Computing
    • SAP
    • Microsoft BizTalk Server
    • Talend
  • Applications Applications logicielles
    • Applications
    • Libres & Open Source
    • OpenOffice & LibreOffice
    • Projets
  • Mobiles Logiciels et matériels mobiles
    • Mobiles
    • Android
    • iOS
    • Windows Phone
  • Systèmes Logiciels et matériels systèmes
    • Systèmes
    • Windows
    • Linux
    • Sécurité
    • PC
    • Mac
    • Réseau
    • Green IT
    • Virtualisation
    • Systèmes Embarqués
  • Programmation
  • Algorithmique
  • 2D-3D-Jeux
  • Assembleur
  • C
  • C++
  • Go
  • Objective C
  • Pascal
  • Perl
  • Python
  • Qt
  • XML
  • Autres
  • Accueil C
  • Forums C
  • FAQ C
  • Tutoriels C
  • Livres C
  • Compilateurs et outils C
  • Sources C
  • Bibliothèques C
  • GTK+

Notes sur le langage C

Table des matières Plier Déplier

  • Page 1
    • I. C'est quoi l'algorithmique ?
    • II. Déclarations ? Définitions ?
      • II-A. Déclaration
      • II-B. Définition
    • III. Langage C ? Fonctions systèmes ? Je suis perdu !
      • III-A. Domaine couvert par le langage C
      • III-B. Fonctions système
      • III-C. Bibliothèques tierces publiques
      • III-D. Bibliothèques tierces privées
    • IV. C, UNIX, POSIX ?
    • V. Production du code exécutable
    • VI. pile, tas ? C'est quoi ?
    • VII. Le mode plein écran
      • VII-A. Installation de ansi.sys sous Windows XP
  • Page 2
    • VIII. Se procurer la norme du langage C (C99)
    • IX. C'est quoi ce 'static' ?
  • Page 3
    • X. Le type retourné par main()
      • X-A. Historique
    • XI. C'est quoi un prototype ?
    • XII. Bibliothèques de fonctions
      • XII-A. Bibliothèque statique
      • XII-B. Bibliothèque dynamique
    • XIII. Pourquoi ma fonction ne modifie pas ma variable ?
    • XIV. Les fonctions à nombre d'arguments variable
      • XIV-A. Introduction
      • XIV-B. Interface
      • XIV-C. Implémentation
      • XIV-D. Conclusion
  • Page 4
    • XV. size_t, c'est quoi ?
    • XVI. Données
      • XVI-A. Initialisation
    • XVII. Structures
      • XVII-A. Structures visibles
      • XVII-B. Structures opaques
      • XVII-C. Pseudonymes (ou alias)
    • XVIII. Variables globales
      • XVIII-A. Nommage
      • XVIII-B. Organisation
      • XVIII-C. Définition
      • XVIII-D. Déclaration
      • XVIII-E. Utilisation
    • XIX. Champs de bits
    • XX. Bien utiliser const
      • XX-A. Objets simples
      • XX-B. Pointeurs
      • XX-C. Usage
  • Page 5
    • XXI. Les pointeurs démythifiés !
      • XXI-A. Introduction
      • XXI-B. Définition
        • XXI-B-1. Pointeur sur objet
        • XXI-B-2. Pointeur de fonction
          • XXI-B-2-a. Remarques importantes
      • XXI-C. Du bon usage des pointeurs
    • XXII. Les pointeurs, ça sert à quoi ?
      • XXII-A. A quoi ça sert?
        • XXII-A-1. Petit rappel.
        • XXII-A-2. Comment faire ?
    • XXIII. Un tableau n'est pas un pointeur ! Vrai ou faux ?
    • XXIV. Passer un tableau à une fonction
      • XXIV-A. Introduction
      • XXIV-B. Tableau à une dimension
      • XXIV-C. Tableau à n dimensions
    • XXV. Retourner un tableau
    • XXVI. char* char**
      • XXVI-A. Introduction
      • XXVI-B. Chaine de caractères
      • XXVI-C. Le type char *
      • XXVI-D. Le type char **
    • XXVII. Initialisation des tableaux de caractères
    • XXVIII. Qu'est-ce qu'une chaine litterale ?
    • XXIX. Bien utiliser malloc()
      • XXIX-A. Suppression du cast
        • XXIX-A-1. Compilateur non ISO
        • XXIX-A-2. Compilateur C++
      • XXIX-B. Déterminer la taille sans le type
    • XXX. Bien utiliser realloc()
    • XXXI. Comment créer un tableau dynamique à deux dimensions ?
  • Page 6
    • XXXII. Saisie de données par un opérateur (stdin)
      • XXXII-A. Introduction
      • XXXII-B. fgetc(), getc(), getchar()
      • XXXII-C. gets()
      • XXXII-D. scanf()
      • XXXII-E. fgets()
      • XXXII-F. Ressources
      • XXXII-G. Comment fonctionne fgetc(stdin) alias getchar()
        • XXXII-G-1. Comportement visible.
        • XXXII-G-2. Comportement interne.
        • XXXII-G-3. Quelques expérimentations.
    • XXXIII. Les fichiers
      • XXXIII-A. Introduction
      • XXXIII-B. Texte ou binaire ?
        • XXXIII-B-1. Fichier texte
        • XXXIII-B-2. Fichier binaire
        • XXXIII-B-3. Modes d'ouverture d'un fichier
        • XXXIII-B-4. Lecture d'un fichier
          • XXXIII-B-4-a. fgetc(), getc()
          • XXXIII-B-4-b. fread()
          • XXXIII-B-4-c. fscanf()
          • XXXIII-B-4-d. fgets()
            • XXXIII-B-4-d-i. Exemple d'utilisation
            • XXXIII-B-4-d-ii. Explication
            • XXXIII-B-4-d-iii. Critique de cet exemple
            • XXXIII-B-4-d-iv. Détection d'une erreur
            • XXXIII-B-4-d-v. Gestion des fins de ligne
            • XXXIII-B-4-d-vi. Exemple amélioré avec détection de la fin de lecture
            • XXXIII-B-4-d-vii. Explication
        • XXXIII-B-5. Écriture dans un fichier
          • XXXIII-B-5-a. fputc(), putc()
          • XXXIII-B-5-b. fwrite()
          • XXXIII-B-5-c. fprintf()
          • XXXIII-B-5-d. fputs()
        • XXXIII-B-6. Bien utiliser les formats de données
          • XXXIII-B-6-a. Format orienté texte
          • XXXIII-B-6-b. Format orienté binaire
      • XXXIII-C. Supprimer un enregistrement dans un fichier binaire
      • XXXIII-D. En guise de conclusion
    • XXXIV. Pourquoi fflush (stdout) ?
  • Page 7
    • XXXV. <time.h> : La gestion du temps
      • XXXV-A. Introduction
      • XXXV-B. Usage
        • XXXV-B-1. Lire l'heure courante
        • XXXV-B-2. Afficher l'heure courante
    • XXXVI. <time.h> : bien utiliser difftime()
    • XXXVII. rand(), srand()... j'y comprends rien...
  • Page 8
    • XXXVIII. Du bon usage de qsort()
      • XXXVIII-A. Description de la fonction qsort()
        • XXXVIII-A-1. Introduction
        • XXXVIII-A-2. Interface
          • XXXVIII-A-2-a. Interface de la fonction de comparaison
        • XXXVIII-A-3. Comportement
          • XXXVIII-A-3-a. Comportement de la fonction de comparaison
      • XXXVIII-B. Usage de la fonction qsort()
        • XXXVIII-B-1. Tri d'un tableau d'entiers
        • XXXVIII-B-2. Tri d'un tableau de chaînes
        • XXXVIII-B-3. Tri d'un tableau de structures
        • XXXVIII-B-4. Tri de nombres réels
    • XXXIX. Les identificateurs réservés
    • XL. Bien gérer la portée des objets et des fonctions
      • XL-A. Fonctions
      • XL-B. Objets
        • XL-B-1. Définition hors d'un bloc
        • XL-B-2. Définition dans un bloc
        • XL-B-3. Masquage (Shadowing)
    • XLI. Du bon usage de assert()
      • XLI-A. Exemple d'utilisation
    • XLII. Comportement indéfini
    • XLIII. Les item-lists
      • XLIII-A. Introduction
      • XLIII-B. Mise en oeuvre
  • Page 9
    • XLIV. Borland C : "floating point formats not linked"
    • XLV. Code standard ? Code portable ? Je suis perdu !
      • XLV-A. "standard" ?
      • XLV-B. "portable" ?
        • XLV-B-1. portabilité absolue
        • XLV-B-2. portabilité relative
      • XLV-C. Bon usage
    • XLVI. Enregistrer une structure
      • XLVI-A. Le format binaire
      • XLVI-B. Le format texte

Ce document est un peu disparate. En effet, il contient une multitude d'articles plus ou moins longs qui sont souvent des réponses que j'ai pu apporter à des questions souvent posées sur les forums.

Je me suis efforcé ensuite de les organiser en chapitres de la façon suivante :

  • Généralités ;
  • Éléments du langage ;
  • Fonctions ;
  • Types et Variables ;
  • Pointeurs, tableaux, chaînes ;
  • Entrées / sorties ;
  • Bibliothèque standard ;
  • Codage ;
  • Divers.

Votre avis et vos suggestions sur cet article nous intéressent !
Alors après votre lecture, n'hésitez pas : Commentez Donner une note à l'article (5)

Lire l'article.

Article lu fois.

L'auteur

Emmanuel Delahaye Site personnel

L'article

Publié le 29 avril 2009 - Mis à jour le 7 mars 2013

Version en ligne

Version PDF Version hors-ligne

ePub , Azw et Mobi

Lire l'article

Liens sociaux

Google Bookmarks ! Share on Google+

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique C : Francis Walter -

Developpez.com

Nous contacter

Participez

Informations légales

Services

Forum C

Blogs

Hébergement

Partenaires

PlanetHoster
Ikoula

Copyright © 2000-2014 - www.developpez.com


Notes sur le langage C

Table des matières>

  • I. C'est quoi l'algorithmique ?
  • II. Déclarations ? Définitions ?
    • II-A. Déclaration
    • II-B. Définition
  • III. Langage C ? Fonctions systèmes ? Je suis perdu !
    • III-A. Domaine couvert par le langage C
    • III-B. Fonctions système
    • III-C. Bibliothèques tierces publiques
    • III-D. Bibliothèques tierces privées
  • IV. C, UNIX, POSIX ?
  • V. Production du code exécutable
  • VI. pile, tas ? C'est quoi ?
  • VII. Le mode plein écran
    • VII-A. Installation de ansi.sys sous Windows XP

I. C'est quoi l'algorithmique ?▲

C'est l'art de traduire un comportement en phrases simples et claires.

Surveiller la température.
Si elle dépasse la consigne, actionner une alarme.

Ces phrases sont ensuite traduites en 'pseudo-code' qui est une sorte de langage de description des comportements (ressemble au Pascal) à base d'actions et de structures de code comme IF-ELSE-ENDIF, SELECT-CASE, REPEAT-UNTIL, WHILE etc.

Sélectionnez
DO
   temperature := read_temperature()
   IF temperature > threshold
      alarm (ON)
   ENDIF
FOREVER

Ce pseudo-code est ensuite traduit facilement en langage d'implémentation (par exemple en C).

Sélectionnez
{
   for (;;)
   {
      int temperature = read_temperature();
      if (temperature > threshold)
      {
         alarm (ON);
      }
   }
}

Pour plus d'informations, consultez les Cours & Tutoriels sur l'algorithmique disponibles sur Developpez.com.

II. Déclarations ? Définitions ?▲

Il y a beaucoup de confusions sur les termes définition et déclaration en C. Voici un petit article qui va s'efforcer de mettre les choses au point.

II-A. Déclaration▲

Une déclaration permet l'utilisation d'un objet ou d'une fonction. Elle doit être préalable à l'utilisation.

Exemples avec un objet x de type int :

Sélectionnez
extern int x;
<...>
{
   x = 2;
}

ou

Sélectionnez
int x;

<...>
{
   x = 2;
}

ou

Sélectionnez
{
   int x;
<...>
   x = 2;
}

Voici des exemples de déclarations de fonctions :

Sélectionnez
int f();

extern int g(void);

static int h();

static int i(int a)
{
}

int j(char *b)
{
}

II-B. Définition▲

Une définition est l'endroit où l'objet ou la fonction sont réellement définis. De l'espace mémoire est alloué à l'objet, les instructions sont fournies à la fonction (entre des {}). Notons que par conséquent, une définition est aussi une déclaration implicite. Il convient donc d'être prudent sur l'emploi des termes. Voici des définitions d'objets :

Sélectionnez
int a;
static float b[12];
{
   struct xxx c;
   static long d;
}

ou de fonction :

Sélectionnez
int f()
{
}

int g(void)
{
}

La dernière forme est une définition avec déclaration implicite sous forme de prototype.

III. Langage C ? Fonctions systèmes ? Je suis perdu !▲

Sur un forum je pose une question sur le langage C et on me répond «va voir sur un forum consacré à ton système». M'enfin, je programme en C, c'est quoi ce cirque ?

III-A. Domaine couvert par le langage C▲

Le langage C, tel qu'il est défini par la norme, est un ensemble de règles d'écriture (syntaxe, sémantique) définissant les éléments du langage, et un ensemble de fonctions regroupées sous le terme générique de 'bibliothèque d'exécution du [langage] C'.

Les domaines couverts par la bibliothèque sont :

  • les flux d'entrée/sorties ;
  • les traitements de chaînes ;
  • les conversions chaîne/binaires ;
  • les fonctions mathématiques ;
  • les algorithmes génériques (tri, recherche) ;
  • la gestion du temps ;
  • (d'autres qui me reviendront plus tard…).

On constate donc qu'un programme C standard permet d'entrer des données à partir de la ligne de commande (paramètres de main()) ou d'un flux entrant (stdin, fichier en lecture), de les traiter 'silencieusement', ou avec une trace vers un flux sortant (stdout, stderr ou un fichier en écriture) et de sortir des données vers une un flux sortant selon le schéma bien connu.

Sélectionnez
+--------+   +------------+   +--------+
| entrée |-->| traitement |-->| sortie |
+--------+   +------------+   +--------+

Exemples typiques :

  • compilateur ;
  • générateur de code ;
  • convertisseur wav -> mp3 ;
  • etc.

Si on cherche d'autres domaines d'applications comme :

  • gestion de l'écran en mode texte ;
  • lecture du clavier sans attente ;
  • impression ;
  • port série ;
  • réseau ;
  • écran graphique ;
  • programmation évènementielle ;
  • programmation multitâches ;
  • interface graphique ;
  • souris ;
  • etc.

il va falloir utiliser des ressources externes au langage C. Bien que ces ressources disposent (entre autres), d'une interface leur permettant d'être appelées par un programme écrit en C, elles ne font pas partie du langage C.

On distingue principalement 3 types de ressources :

  • les fonctions fournies par le système ;
  • les fonctions des bibliothèques publiques (gratuites, payante) ;
  • les fonctions des bibliothèques privées (personnelles, entreprises).

III-B. Fonctions système▲

Rappelons qu'un système est un logiciel lancé par la machine au démarrage et qui prend en charge la gestion des ressources matérielles, ainsi que la surveillance de différents événements. D'autre part, il fourni un certain nombre de fonctions utilisables dans les applications, ainsi que le moyen de charger et lancer (exécuter) une ou des applications.

L'ensemble des interfaces des fonctions système utilisables pour développer des applications est décrit dans un document appelé API (Application Programming Interface). Ce document précise pour chaque fonction :

  • identificateur ;
  • paramètres ;
  • retour ;
  • comportement.

Pour des raisons propres à chaque architecture, l'interface est souvent matérialisée par un 'TRAP' ou une interruption logicielle que l'on ne peut appeler qu'en assembleur. Par exemple en sur un PC/x86, l'interruption BIOS 'Vidéo' :

Sélectionnez
MOV AH, 09h
   MOV AL, character
   MOV BH, 00h
   MOV BL, attributes
   MOV CX, 01h
   INT 10h

ou avec des extensions de très bas niveau comme par exemple ceci en Borland C :

Sélectionnez
#if defined (__BORLANDC__)
#include <dos.h>
#else
#error Undefined for this platform
#endif

<...>

/* ---------------------------------------------------------------------
   VIDEO_putch()
   ---------------------------------------------------------------------
   écriture d'un caractère a la position courante du curseur avec
   la couleur specifiee
   ---------------------------------------------------------------------
   E : caractère (0-255)
   E : couleur texte
   E : couleur fond
   S :
   --------------------------------------------------------------------- */
void VIDEO_putch (int c, eCOU ct, eCOU cf)
{
#if defined (__BORLANDC__)
   union REGS reg;

   /* caractere a écrire */
   reg.h.al = (uchar) c;

   /* putch avec attribut */
   reg.h.ah = 0x09;

   /* page 0 */
   reg.h.bh = 0;

   /* attributs 08h : clignotement */
   reg.h.bl = (uchar) (ct | ((cf & ~0x08) << 4));

   /* pas de repetitions */
   reg.x.cx = 1;

   /* Bios Video */
   int86 (0x10, &reg, &reg);
#endif
}

Afin de faciliter l'appel à partir de différents langages de développement, chaque implémenteur de compilateur (C, C++, Pascal, Ada etc.) ou d'interpréteur (BASIC, Python, Ruby etc.) fournit une interface dans son langage. Les fonctions systèmes apparaissent donc comme une extension dudit langage.

Chaque système dispose de sa propre API. Mais certains systèmes offrent des API définies selon la norme POSIX, ce qui tend à normaliser au moins une partie des API.

III-C. Bibliothèques tierces publiques▲

Il est aussi possible d'utiliser des fonctions fournies par des bibliothèques publiques gratuites ou payantes selon les besoins et les licences requises. Ces bibliothèques sont le plus souvent indépendantes de la plate-forme. On peut citer (interface C) :

  • GTK+ (GUI) ;
  • SDL (Graphisme 2D simple) ;
  • Fmod (Multimédia) ;
  • MySQL (SGBD) ;
  • etc.

III-D. Bibliothèques tierces privées▲

Chaque développeur peut, pour lui-même, ou au sein de son entreprise, développer une bibliothèque de fonctions 'métier' qui lui facilitent la réalisation d'applications spécialisées. Il est courant que ces bibliothèques soient partagées dans l'entreprise.

Certaines de ces bibliothèques peuvent ensuite devenir publiques si elles peuvent intéresser d'autres personnes et si la licence le permet.

IV. C, UNIX, POSIX ?▲

Le langage C n'a à voir ni avec UNIX ni avec POSIX.

UNIX est une norme de spécification de système (actuellement UNIX 03) gérée par l'Open Group qui définit un système indépendamment de la machine sur lequel il tourne. Elle s'appuie, entre autre, sur la spécification des fonctions systèmes définie par POSIX.1 sous l'égide de l'IEEE.

Cette norme (POSIX.1) comprend à la fois la description d'un certain nombre de fonctions systèmes, et d'au moins deux types d'interfaces :

  • le mode commande (shell) pour l'utilisateur de la machine ;
  • une API utile aux programmeurs d'applications.

L'API est décrite sous la forme d'interfaces de fonctions appelables directement en langage C (C99 pour les dernières spécifications de POSIX.1). Les fonctions standards du C sont reprises telles quelles par POSIX.1.

Ce sont ces nombreux emprunts au langage C qui ont créés la confusion entre langage C et POSIX…

Cette API est implémentée par une bibliothèque (.a, .so, .lib, .dll, ...) et des fichiers d'entête (.h) livrés avec les systèmes compatibles POSIX.1 (la plupart des unixoïdes, mais aussi certains Windows).

La spécification complète de POSIX.1 est disponible ici (s'inscrire, c'est gratuit et sans danger).

Les constructeurs de systèmes et de compilateurs s'efforcent de suivre tout ou partie de ces normes, ce qui permet une portabilité importante dans des domaines qui ne sont pas couverts par la norme du langage C comme :

  • la gestion des répertoires ;
  • la gestion des processus (process) ;
  • la gestion des tâches (threads) ;
  • la gestion des réseaux (sockets) ;
  • etc.

V. Production du code exécutable▲

Il existe de nombreuses implémentations du langage C. Certaines sont des interpréteurs, mais la plupart sont des compilateurs.

Compiler un programme consiste à vérifier le code source, puis à le traduire en langage machine de façon à en faire un fichier exécutable qui pourra ensuite être exécuté par la machine.

Les détails de production du code dépendent de l'implémentation, c'est à dire de la machine, du système, des outils utilisés etc. Cependant, il existe une procédure générale commune à toutes ces implémentations :

  • production de modules de code machine intermédiaires par compilation individuelle des codes sources. Il manque les références externes ;
  • production du code machine exécutable par résolution des références externes (liens) entre les modules intermédiaires, et l'éventuelle ajout de bibliothèques de fonctions selon les besoins.

Pour cela, on utilise successivement deux outils :

  • le compilateur (compiler), autant de fois que nécessaire, selon le nombre de fichiers sources à traiter ;
  • l'éditeur de lien (linker), une fois pour produire l'exécutable.

VI. pile, tas ? C'est quoi ?▲

Le langage C définit 3 zones de stockage pour les objets (aussi appelés variables)

  • la mémoire statique ;
  • la mémoire allouée ;
  • la mémoire automatique.

1 - Les objets définis hors des fonctions et les objets définis avec le mot clé static sont placés en mémoire statique. Leur durée de vie est celle de l'exécution du programme. Ils existent et sont initialisés (à 0 par défaut) avant même le lancement de main().

2 - Les objets définis avec les fonctions malloc(), calloc() et realloc() sont placés en mémoire allouée (appelée heap ou tas sur certaines implémentations). Leur durée de vie est contrôlée par le programme. Ils existent dès que l'appel de *alloc() retourne une valeur différente de NULL et cessent d'exister dès que free() est appelé avec la valeur retournée par *alloc().

3 - Les objets définis dans un bloc, sans qualificateur 'static', sont placés en mémoire automatique (appelée stack ou pile sur certaines implémentations). Leur durée de vie est celle du bloc dans lequel ils sont définis.

VII. Le mode plein écran▲

En C standard, la notion d'écran (et plus généralement de matériel) n'existe pas. Une application écrite en C est censée tourner sur une machine abstraite dont les interfaces matérielles sont vues comme des flux d'entrée ou de sortie. Ceci dans un souci de portabilité, qui est une des raisons d'être du langage C.

Parmi ces flux, il en existe 3 qui sont ouverts par défaut :

  • stdin : entrée standard ;
  • stdout : sortie standard ;
  • stderr : sortie erreur.

Une application écrite en C peut être recompilée pour une multitude de plate-formes (qui diffèrent selon le processeur, l'architectures, le système, etc.) pour lesquelles stdin, stdout et stderr seront implémentés de manières différentes.

Sur un modem, par exemple, ces flux seront connectés à un port série permettant de brancher une console par laquelle on pourra passer, par exemple, des commandes AT.

Le but est d'instaurer un dialogue 'interactif' dans lequel l'application montre qu'elle attend une commande de l'opérateur à l'aide d'un 'prompt' ou 'invite de commande' :

Sélectionnez
modem> _

L'utilisateur peut alors passer une commande de numérotation :

Sélectionnez
modem> ATD0123456789
_

et le modem répond :

Sélectionnez
CONNECTED AT 9600 BAUDS

Il passe alors en mode transmission de données, et il signale qu'il n'attend plus de commandes par l'absence de prompt.

De même, sur un PC sous Windows ou unixoide, il est possible d'ouvrir une console localement ou de se brancher localement ou à distance sur la machine via une console (gestion, application, debug...). L'application ne verra pas la différence et continuera à attendre ses commandes de stdin et à envoyer ses réponses à stdout et/ou stderr.

L'avantage d'un tel système est qu'il exige très peu de la console, qui peut se réduire à un simple terminal série, voire à une imprimante série s'il ne s 'agit que de traces sur stdout ou stderr (utile pour mettre au point une application graphique, par exemple).

La gestion plein écran est une autre façon de concevoir la relation entre l'homme et la machine (IHM). En effet, on peut reprocher à l'interface console 'interactive', un certain manque de convivialité (largement compensé par le développement de langages 'scripts' extrêmement puissants), rendant les applications difficiles d'accès à un public non informaticien (direction, comptables, secrétaires, achats, commerciaux etc.)

C'est pour cela qu'il a été développé la notion d'interface 'plein écran' qui permet d'améliorer l'ergonomie :

  • effacement de l'écran ;
  • placement absolu du curseur ;
  • contrôle de la couleur du texte et du fond.

Malheureusement, il n'a jamais été prévu de norme internationale régissant cette gestion d'écran (probablement parce que faisant trop partie du domaine applicatif, chaque constructeur voulant tirer parti des performances de son matériel et de son logiciel).

Certaines pratiques et normes de fait se sont toutefois imposées.

  • Basées sur les ressources de la machine :
    • PC/MS-DOS : Borland et sa bibliothèque conio (Turbo Pascal, C) pour la console intégrée. Windows bénéficie d'un portage réduit de conio pour MinGW (voir la documentation) ;
    • Unixoides : curses et ncurses. (consoles locales).
  • Basées sur des ressources console :
    • Commandes ANSI (VT-100) ;
    • Vidéotex (Minitel) ;
    • HTML (Browser).

Je recommande la bibliothèque PDCurses qui est une version portable et multi-plateforme de [n]curses et qui permet facilement de gérer les entrées/sorties directes à la console, et ce soit d'une manière basique à-la-conio, ou d'une manière plus avancée. Il existe de nombreux tutoriels sur le net.

Mention spéciale pour 'termcaps' qui est une bibliothèque multi-plateforme censée s'adapter à quasiment tous les terminaux supportant des séquences d'échappement.

VII-A. Installation de ansi.sys sous Windows XP▲

A ma connaissance, il n'y a pas de possibilité d'installer un interpréteur de séquences ANSI pour cmd.exe.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • VIII. Se procurer la norme du langage C (C99)
  • IX. C'est quoi ce 'static' ?

VIII. Se procurer la norme du langage C (C99)▲

La norme du C est définie par l'ISO. On trouve l'original sur le site de l'ISO, mais elle est très chère. Elle est aussi reprise par l' ANSI à l'identique, pour la somme raisonnable de 20 USD.

Il est possible de télécharger gratuitement le dernier draft de la norme (N1256) ici : (C99). Ce document est complet et intègre C99, TC1 et TC2 (Technical Corrigendum 1 et 2). C'est probablement le dernier draft du document définitif de l'ultime norme du C qui ne devrait plus évoluer, le comité étant en sommeil.

En fait, le comité continue de travailler, et une nouvelle version du langage C (C1x) est en cours d'élaboration. En voici le dernier draft : n1336.

Il existe un document quasiment officiel qui commente la norme : Le 'Rationale'. Très utile pour trouver des explications complémentaires.

IX. C'est quoi ce 'static' ?▲

static est un qualificateur qui a plusieurs significations selon le contexte :

  • devant une déclaration de fonction :
Sélectionnez

static int f(void)

Limite la portée de la fonction à l'unité de compilation courante.

  • devant une variable définie hors de tout bloc (dite 'de portée globale') :
Sélectionnez

static int x;

Limite la portée de la variable à l'unité de compilation courante.

  • devant une variable définie dans une bloc (dite 'de portée locale') :
Sélectionnez

int counter(void)
{
   static int x;
   x++;
   return x;
}

Place la variable dans la mémoire statique, la rendant persistante. Usage rarissime (quick'n dirty).

A éviter, surtout pour du code réutilisable (bibliothèque).

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • X. Le type retourné par main()
    • X-A. Historique
  • XI. C'est quoi un prototype ?
  • XII. Bibliothèques de fonctions
    • XII-A. Bibliothèque statique
    • XII-B. Bibliothèque dynamique
  • XIII. Pourquoi ma fonction ne modifie pas ma variable ?
  • XIV. Les fonctions à nombre d'arguments variable
    • XIV-A. Introduction
    • XIV-B. Interface
    • XIV-C. Implémentation
    • XIV-D. Conclusion

X. Le type retourné par main()▲

Bien que main() soit censé retourner un int, on voit quelquefois écrit

Sélectionnez

void main (void)
{
}

Qu'en est-il exactement ?

D'après la définition du langage C, dans un programme conforme, main() doit retourner int. D'ailleurs un compilateur comme Borland C 3.1 en mode ANSI refuse void main() (error). Dans les mêmes conditions, gcc qui émet un avertissement (warning).

X-A. Historique▲

Dès l'apparition du langage C, une des formes canoniques de main() était :

Sélectionnez

main()
{
   return 0;
}

l'autre étant la forme qui permet de récupérer les arguments de la ligne de commande.

Il faut bien comprendre qu'à cette époque, une fonction définie sans type de retour explicite, retournait un int (c'est toujours le cas en C90, mais plus en C99 où le type doit être explicite). Le mot clé 'void' n'existait pas.

Il n'y avait donc aucune raison d'utiliser une forme void main().

Ensuite, est venue la normalisation du langage C. (1989 ANSI, 1990 ISO). Dans le texte, les deux formes canoniques sont décrites :

Sélectionnez

int main (void)

et :

Sélectionnez

int main (int argc, char **argv)

Il est précisé en remarque (dans la partie non normative) qu'il existe d'autres formes sans autres précisions. Elles ne font donc pas partie de la norme, leur comportement est donc indéfini dans le cadre d'un programme respectueux de la norme (dit 'strictement conforme').

XI. C'est quoi un prototype ?▲

Lorsqu'on défini une fonction :

Sélectionnez

int f (char *s)
{
  ...
}

on définit en même temps son interface, à savoir :

  • son nom (ici , f) ;
  • son type de retour (ici, int) ;
  • ses paramètres (ici, s, de type pointeur sur char).

Cet interface est aussi appelé prototype intégré. Il permet une utilisation de la fonction si l'appel est placé après la définition (il n'y a pas de raison de faire autrement, sauf cas exceptionnels et souvent douteux...) :

Sélectionnez

int f (char *s)
{
  ...
}

int main (void)
{
   int x = f("hello");
}

Maintenant, dans le cas de compilation séparée, on doit 'détacher' le prototype et le placer dans un fichier qui sera inclus à la fois dans le fichier d'implémentation de la fonction et dans le ou les fichiers qui l'utilisent.

Sélectionnez

/* f.h */
int f (char *s);
Sélectionnez

/* f.c */
#include "f.h"
int f (char *s)
{
  ...
}
Sélectionnez

/* main.c */
#include "f.h"

int main (void)
{
   int x = f("hello");
}

Pour compléter, il est fortement recommandé de protéger les fichiers .h contre les inclusions multiples avec une garde (guard) :

Sélectionnez

#ifndef H_F
#define H_F
/* f.h */

int f (char *s);

#endif

Détails dans cet article ici

XII. Bibliothèques de fonctions▲

Une bibliothèque (library) est une collection de fonctions mise à la disposition des programmeurs. Elle se compose d'une interface matérialisée par un ou plusieurs fichiers d'entêtes (.h), et d'un fichier d'implémentation qui contient le corps des fonctions (.lib, .a, .dll, .so etc.) sous forme exécutable.

Il est aussi possible à un programmeur de 'capitaliser' son travail en réalisant des fonctions réutilisables qu'il peut ensuite organiser en bibliothèques. Cette pratique est courante et encouragée. La création physique d'une bibliothèque est assez simple si on respecte quelques règles de conception, comme l'absence de globales, la souplesse et l'autonomie (voir l'article Comment construire sa bibliothèque). Elle nécessite un outil spécialisé (librarian) généralement livré avec toute implémentation du langage C.

Une bibliothèque peut être statique ou dynamique (partagée).

XII-A. Bibliothèque statique▲

Une bibliothèque à édition de lien statique (.lib, .a etc.) est liée à l'application pour ne former qu'un seul exécutable. La taille de celui-ci peut être importante, mais il a l'avantage d'être autonome. Cette pratique a l'avantage de la simplicité, et elle ne requiert aucune action particulière de la part d'un éventuel système lors de l'exécution du programme. Elle est très utilisée en programmation embarquée (embedded).

XII-B. Bibliothèque dynamique▲

Une bibliothèque à édition de lien dynamique, est un fichier séparé (.dll, .so, ...) qui doit être livré avec l'exécutable. L'intérêt est que plusieurs applications peuvent se partager la même bibliothèque, ce qui est intéressant, surtout si sa taille est importante. Dans ce cas, les exécutables sont plus petits. Autre avantage, les défauts de la bibliothèque peuvent être corrigés indépendamment des applications. Ensuite, la correction est répercutée immédiatement sur toutes les applications concernées par simple mise à jour de la bibliothèque dynamique.

Une application qui utilise une bibliothèque dynamique doit réaliser le lien avec la bibliothèque à l'exécution. Pour cela, elle utilise des appels à des fonctions système spécifiques dans sa phase d'initialisation. Les détails dépendent de la plate-forme.

XIII. Pourquoi ma fonction ne modifie pas ma variable ?▲

Il y a deux façon de modifier la valeur d'un objet (aka variable) avec une fonction :

  • récupérer la valeur retournée par la fonction :
Sélectionnez
int x = f();

ou :

Sélectionnez
int x;
x = f();

avec :

Sélectionnez
int f(void);
  • passer l'adresse de la variable dont on veut modifier la valeur à la fonction :
Sélectionnez
int x;
f(&x);

avec :

Sélectionnez
void f(int *);

Si l'objet est un pointeur, la règle est la même :

  • retourner une valeur :
Sélectionnez
int *px = f();

avec :

Sélectionnez
int *f(void);
  • passer l'adresse :
Sélectionnez
int *px;
f(&px);

avec :

Sélectionnez
void f(int **);

Il n'y a aucune chance de modifier l'entier en faisant ceci :

Sélectionnez
int x;
f(x);

De même, il n'y a aucune chance de modifier le pointeur en faisant cela :

Sélectionnez
int *px;
f(px);

XIV. Les fonctions à nombre d'arguments variable▲

XIV-A. Introduction▲

Bien que leur usage ne soit pas recommandé, car il n'y a pas ou peu de contrôles possibles par le compilateur, le langage C supporte les fonctions à nombre variable d'arguments (variadic).

Attention ! Non seulement le nombre est variable, mais aussi le type.

Un exemple bien connu est printf() :

Sélectionnez
printf ("s:%d\n", __FILE__, __LINE__);

XIV-B. Interface▲

  • la fonction admet n'importe quel type de retour ;
  • elle a obligatoirement un paramètre fixe ;
  • les paramètres variables marqués par l'ellipse '...', sont obligatoirement les derniers.

Ces prototypes sont valides :

Sélectionnez
void f(int a, ...);
int f(int a, char *b, ...);
/* etc. */

Le rôle du dernier paramètre fixe est de fournir un moyen de déterminer le nombre et éventuellement le type des paramètres variables (donc passés après).

Exemple :

Sélectionnez
void f (int n, ...);

<...>
{
   f(3, 'a', 'b', 'c');
}

Ici, le dernier paramètre fixe indique le nombre de caractères passés.

Un effort particulier doit être fait au niveau de la documentation de la fonction, car il n'y a pas de contrôle possible de la part du compilateur. Si on se trompe de type ou de nombre, c'est le drame…

XIV-C. Implémentation▲

Elle varie évidemment selon la fonction à réaliser, mais elle s'appuie sur quelques principes communs :

  • inclure <stdarg.h> pour accéder aux fonctions et macros de manipulation des paramètres variables
  • définir une variable de traitement des variadics de type va_list
  • initialiser cette variable correctement avec va_start() et le nom du dernier paramètre fixe
  • récupérer et traiter les paramètres selon la spécification de la fonction
  • terminer l'analyse proprement avec va_end()
Sélectionnez
#include <stdarg.h>

void f (int n, ...)
{
   va_list va;
   va_start (va, n);

   /* traitement des paramètres selon la fonction */

   va_end (va);
}

La récupération des paramètre se fait séquentiellement de gauche à droite par appels successifs à va_arg() en précisant le type attendu, parmi 'int', 'long', 'double', 'void*' et 'char*' [C90].

Sélectionnez
int x = va_arg (va, int);
double y = va_arg (va, double);
char *z = va_arg (va, char *);
/* etc. */

Il doit y avoir correspondance parfaite entre le type de la variable et celui passé en paramètre à la macro va_arg().

Là encore, la vigilance est de rigueur, car aucun contrôle n'est possible de la part du compilateur.

Exemple d'implémentation de la fonction présentée au-dessus :

Sélectionnez
#include <stdarg.h>
#include <stdio.h>

void f (int n, ...)
{
   va_list va;
   va_start (va, n);

   int i;

   for (i = 0; i < n; i++)
   {
      int c = va_arg (va, int);
      putchar (c);
   }
   putchar ('\n');
   va_end (va);
}

int main (void)
{
   f (3, 'a', 'b', 'c');
   f (4, 'x', 'y', 'z', 't');

   return 0;
}
Sélectionnez
abc
xyzt

Process returned 0 (0x0)   execution time : 0.025 s
Press any key to continue.

XIV-D. Conclusion▲

Nous avons jeté les bases de la construction des fonctions variadics. Attention, ces fonctions sont une source importante de problèmes invisibles au yeux du compilateur.

Ne pas les utiliser dans des projets critiques sans une procédure de validation de l'implémentation et de l'usage extrêmement rigoureuse. Dans certains domaines sensibles (médical, avionique, nucléaire), leur usage est tout simplement interdit.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XV. size_t, c'est quoi ?
  • XVI. Données
    • XVI-A. Initialisation
  • XVII. Structures
    • XVII-A. Structures visibles
    • XVII-B. Structures opaques
    • XVII-C. Pseudonymes (ou alias)
  • XVIII. Variables globales
    • XVIII-A. Nommage
    • XVIII-B. Organisation
    • XVIII-C. Définition
    • XVIII-D. Déclaration
    • XVIII-E. Utilisation
  • XIX. Champs de bits
  • XX. Bien utiliser const
    • XX-A. Objets simples
    • XX-B. Pointeurs
    • XX-C. Usage

XV. size_t, c'est quoi ?▲

size_t est le type retourné par l'opérateur sizeof. C'est un entier non signé. Il est suffisamment grand pour contenir la valeur représentant, en nombre de bytes (ou char), la taille du plus grand objet possible d'une implémentation donnée.

Il convient pour les tailles, les dimensions de tableau, les index croissants et non négatifs…

Ce type est défini dans <stddef.h> qui est inclus dans la plupart des headers standards courants (<stdio.h>, <stdlib.h> <string.h> etc.)

XVI. Données▲

Le langage C utilise 3 zones mémoire pour implémenter les données :

  • la mémoire statique, qui contient les variables permanentes (modifiables ou non) ;
  • la mémoire automatique qui contient les variables locales et les paramètres des fonctions ;
  • la mémoire allouée qui contient des variables dynamiques gérées à l'exécution par le programme (malloc() / free().

Les données en C sont caractérisées par :

  • leur portée ;
  • leur durée de vie.

La portée peut être :

  • locale à un bloc ;
  • limitée à une unité de compilation ;
  • illimitée ;
  • contrôlée par l'application.

La durée de vie peut être :

  • limitée à un bloc ;
  • permanente ;
  • contrôlée par l'application.

Exemples :

Sélectionnez
/* permanente de portée illimitée */
int a;

/* permanente de portée limitée à l'unité de compilation */
static int b;

<...>
{
/* locale (de portée limitée au bloc) */
   int c;

/* permanente de portée limitée au bloc */
   static int d;

/* contrôlée de portée limitée à la validité du pointeur (non NULL) */
   int *p = malloc (sizeof *p * 3);

   free (p), p = NULL;
}

XVI-A. Initialisation▲

Seules les données statiques sont initialisées avant le lancement de main(). Les autres ont une valeur indéterminée. Il est donc nécessaire de les initialiser avant utilisation (lecture). Sauf indication explicite contraire, l'initialisation par défaut des données statiques est 0.

XVII. Structures▲

XVII-A. Structures visibles▲

On appelle structure visible une structure dont les éléments sont visibles de l'utilisateur.

Une 'définition de structure' est le moyen par lequel le programmeur indique au compilateur comment est constitué une structure. Cette opération ne réserve aucune mémoire.

Sélectionnez
struct mastructure
   {
      type_1 element_a;
      type_2 element_b;
   };

Ensuite, une structure peut être instanciée, c'est à dire qu'une instance de cette structure est définie en mémoire.

Sélectionnez
struct mastructure mastructure;

il est autorisé d'utiliser le même nom, même si ce n'est probablement pas le meilleur des choix possible.

Ces informations suffisent à définir et instancier n'importe quelle structure 'visible'.

XVII-B. Structures opaques▲

On appelle structure opaque une structure dont les éléments ne sont pas visibles de l'utilisateur.

Pour cela, on utilise une définition réduite (ou incomplète) qui consiste à définir le nom de la structure sans en préciser le contenu.

Sélectionnez
struct mastructure;

Cette définition dite incomplète ne permet évidemment pas de créer un instanciation, puisque le compilateur ignore le contenu de la structure. Il n'a donc pas les moyens d'en déterminer la taille.

Par contre, il est possible de créer un pointeur de ce type :

Sélectionnez
struct mastructure *p;

Il devient alors possible de créer une fonction qui retourne un pointeur de ce type :

Sélectionnez
struct mastructure *fonction(void);

de passer ce pointeur en paramètre une fonction :

Sélectionnez
void fonction(struct mastructure *p);

d'en faire un élément de structure etc.

Évidemment, il faudra que la structure soit définie 'quelque part' afin qu'elle soit instanciable et que ses éléments soient manipulables.

On va donc créer un fichier source (.c) séparé d'implémentation contenant les fonctions permettant la création (instanciation) des données, et une interface (header ou .h) ne comportant que la définition incomplète de la fonction et, au minimum, les 2 fonctions permettant la création et la suppression d'une instance de la structure.

Soit la structure 'xxx'. On obtient :

Sélectionnez
/* xxx.h Interface */
#ifndef H_XXX
#define H_XXX

/* définition incomplète de structure */
struct xxx;

/* prototypes des fonctions */
struct xxx *xxx_create (void);
void xxx_delete (struct xxx *p);
/* etc. */

#endif
Sélectionnez
/* xxx.c Implémentation */
#include "xxx.h"

/* définition de la structure (exemple) */
struct xxx
{
   int a;
   char b[10];
};

/* fonctions publiques */
struct xxx *xxx_create (void)
{
   /* a completer */
}

void xxx_delete (struct xxx *p)
{
   /* a completer */
}

Exemple d'utilisation :

Sélectionnez
/* test.c */
#include "xxx.h"

#include <stddef.h>

int main (void)
{
   /* instanciation */
   struct xxx *p = xxx_create ();

   /* tout appel de fonction peut échouer... */
   if (p != NULL)
   {
      /* utilisation de l'objet xxx
         (via de nouvelles fonctions à créer)

         ...
       */

      /* fin d'utilisation : destruction de l'objet */
      xxx_delete (p), p = NULL;
   }
   return 0;
}

Je laisse au lecteur le soin de proposer une ou des implémentations de xxx_create() et de xxx_delete(), sachant qu'on a pas forcément besoin d'un nombre illimité d'instanciations…

XVII-C. Pseudonymes (ou alias)▲

Il est possible, afin de simplifier l'écriture (notamment pour les interfaces publiques), de remplacer le nom de la structure par un nom différent (alias ou pseudonyme) généralement plus court. Il est recommandé de ne pas abuser de l'abstraction, car les possibilités sont réduite en C et il est bon que le programmeur garde en tête qu'il manipule des pointeurs.

Ceci est possible :

Sélectionnez
typedef struct xxx xxx_s;

mais ceci est déconseillé :

Sélectionnez
typedef struct xxx *xxx; /* /!\ */

Détails d'application dans l'article Les types abstraits de données (ADT).

XVIII. Variables globales▲

Une variable globale est une variable définie en dehors d'une fonction et de portée globale. Sa durée de vie est égale à celle du programme. Elle est initialisée par défaut (0) ou explicitement avant l'exécution de main(). Sa valeur est persistante.

Un usage abusif des variables globales est fortement déconseillé pour diverses raisons :

  • on ne sait ni comment ni quand ni qui accède à cette variable. Cela rend le code intestable et incompréhensible. On n'a aucune certitude sur le code ;
  • cela crée une dépendance, ce qui le code rend non modulaire et non réutilisable ;
  • l'instance étant unique, ça rend le code impropre à la récursion et à l'utilisation dans des threads et même au simple appel imbriqué.

Ceci dit, il est des cas rares (ou plus fréquents s'il s'agit de lecture seule) où les variables globales sont utiles, voire indispensables. Dans le cadre d'une application professionnelle, ces cas doivent être justifiés. Voici comment les définir correctement dans le cadre d'une application composée d'unités de compilations séparées.

XVIII-A. Nommage▲

Il est recommandé d'utiliser le préfixe g_ ou G_ pour signifier qu'une variable est globale.

XVIII-B. Organisation▲

Il est préférable pour éviter la dispersion, d'utiliser une ou des structures de variables globales regroupées par fonction, plutôt qu'une multitude de variables.

XVIII-C. Définition▲

Il est recommandé que la définition d'une variable globale soit faite exclusivement dans un fichier source (.c). Ce fichier doit inclure le fichier de déclaration (en-tête).

Sélectionnez
/* data.c */

#include "data.h"

int G_x;

/* la taille du tableau est définie
 * dans la déclaration.
 */
double G_a[];

data_s G_data;

XVIII-D. Déclaration▲

Il est recommandé que la déclaration d'une variable globale soit faite exclusivement dans un fichier d'entête (.h). Ce fichier doit être inclus dans le fichier de définition et dans tous les fichiers d'utilisation (.c). Comme tous les fichiers d'en-têtes, celui-ci dispose de protections contre les inclusions multiples.

Sélectionnez
#ifndef H_DATA
#define H_DATA

/* data.h */

typedef struct
{
   int a;
   char b[123];
}
data_s;

extern int G_x;

/* la dentition de la taille du tableau est unique. Elle est faite ici. */
extern double G_a[12];

extern data_s G_data;

#endif /* guard */

XVIII-E. Utilisation▲

Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).

Sélectionnez
/* appli.c */

#include "data.h"

int main (void)
{
   G_x = 123;

   G_data.a = 456;

   G_a[3] = 123.456;

   return 0;
}

XIX. Champs de bits▲

Afin de réduire la taille des objets, il est possible de définir un champ de bits. La définition doit se faire dans une structure. Le type de l'objet unitaire doit être int ou unsigned int (recommandé) ou _Bool (bool) en C99.

Sélectionnez
typedef struct
{
   unsigned a:1; /* variable de largeur 1 bit */
   unsigned b:3; /* variable de largeur 3 bits */

   /* etc. */
}
data_s;

Il faut garder à l'esprit que l'implantation mémoire des bits n'est pas spécifiée par le langage C. (Et j'ai effectivement constaté sur le terrain des différences selon les implémentations, notamment concernant l'ordre des bits).

Autant une utilisation interne est possible et peut se justifier pour réduire la taille des objets (stockage en mémoire, notamment), extrait de CLIB Module DATE (date.h) (Les tailles indiquées en commentaire sont les tailles minimales garanties)…

Sélectionnez
typedef unsigned int uint;
<...>
typedef struct
{
   /* 16 bits */
   int year;        /* -32767..+32767 */
   uint month:4;    /* 0-15 */
   uint day:5;      /* 0-31 */

   uint hour:5;     /* 0-31 */
   uint minute:6;   /* 0-63 */
   uint second:6;   /* 0-63 */
}
sDATE;

… autant il est illusoire d'utiliser les champs de bits pour créer une interface avec l'extérieur du programme, comme un flux de bytes ou un périphérique en accès direct (mémoire, bus I/O etc.).

Autre pratique non portable, faire une union entre un champ de bits et une variable en s'imaginant pouvoir accéder à la variable, soit d'un bloc, soit bit à bit.

La solution portable pour accéder aux bits d'une variable est d'utiliser les opérateurs binaires (&, |, ~, <<, >>, ^)

XX. Bien utiliser const▲

Voici à quoi sert le qualificateur const et comment l'utiliser correctement.

XX-A. Objets simples▲

Le mot clé 'const' est un qualificateur (qualifier) d'objet. Il lui fait perdre sa qualité par défaut qui est 'accessible en lecture ou en écriture' pour le modifier en 'accessible en lecture seule'. Par exemple :

Sélectionnez
int x = 3;
int const y = 4;

x = 5; /* accès en écriture possible */
y = 6; /* accès en écriture interdit */

Le compilateur signale l'erreur.

On peut placer indifféremment le qualificateur const avant ou après le type.

Sélectionnez
int const a = 7;
const int b = 8;

Mais je conseille néanmoins la première forme, car elle est beaucoup plus claire (notamment avec les pointeurs).

Il est techniquement possible de définir un objet const non initialisé :

Sélectionnez
int const c;

Évidemment, l'intérêt est limité, mais il a son application dans un contexte particulier : les paramètres de fonctions.

XX-B. Pointeurs▲

Le cas des pointeurs est un peu plus complexe, puis qu'il y a en quelque sorte deux objets pour le prix d'un !

  • le pointeur lui même qui peut être qualifié const ;
  • l'objet pointé qui peut lui même être qualifié const.

Pour le pointeur, celui-ci étant un objet comme autre, la même règle s'applique, sachant que const doit être placé juste avant l'identificateur, c'est à dire après le dernier * :

Sélectionnez
int a;
int * const pa = &a;

pa++; /* interdit */
*pa = 123; /* OK */

Mais un autre qualificateur const peut être utilisé pour préciser les droits du pointeur sur l'objet pointé.

Celui ci se place à la gauche de l'*, avant ou après le typem :

Sélectionnez
int a = 123;
int const * pa = &a;
const int * pb = &a;

mais là encore, pour des question de clarté du code, je recommande la première forme. Ce qualificateur interdit la modification de l'objet via le pointeur (mais s'il n'est pas lui même qualifié const, l'objet reste modifiable directement, évidemment).

Sélectionnez
int a = 123;
int const * pa = &a;

*pa = 456; /* interdit */
a = 456; /* OK */

Par contre, attention. Il est techniquement possible de définir un pointeur sur un objet qualifié const et de tenter de modifier l'objet. Cela produit un comportement indéfini qui n'est pas forcément signalé par le compilateur.

Sélectionnez
int const a = 123;
int * pa = &a;

*pa = 456; /* comportement indéfini */

Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.

Sélectionnez
int * pa = &a;

mais il convient de rester extrêmement prudent. Le C est un langage qui demande rigueur et maîtrise.

Bien évidemment, le typecast n'est pas la solution :

Sélectionnez
int const a = 123;
int * pa = (int *) &a; /* NE PAS FAIRE CECI */

il ne fait éventuellement que masquer le problème au compilateur ("je sais ce que fais"), mais il ne résout rien et le comportement indéfini est toujours là.

XX-C. Usage▲

Le rôle du qualificateur const est particulièrement utile avec les pointeurs, notamment sur des chaînes de caractères, qui, rappelons le, ne sont pas modifiables. Il est fortement recommandé de définir tout pointeur sur une chaîne de caractères avec le qualificateur const :

Sélectionnez
char const *p = "hello";

Il est utile aussi pour les pointeurs passés en paramètre à des fonctions. Il permet en effet de restreindre l'accès à la variable pointée à un mode 'lecture seule', ce qui évite bien des erreurs de codage. (La modification d'une variable étant une opération lourde de conséquences si elle est faite au mauvais moment).

Une fonction qui affiche le contenu d'un tableau ou d'une structure, par exemple, n'a pas à la modifier. On fixe donc les règles du jeu dès la définition du prototype :

Sélectionnez
void display (T const *p)

Une utilisation astucieuse et intelligente du qualificateur const permet d'écrire du code plus sûr.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XXI. Les pointeurs démythifiés !
    • XXI-A. Introduction
    • XXI-B. Définition
      • XXI-B-1. Pointeur sur objet
      • XXI-B-2. Pointeur de fonction
        • XXI-B-2-a. Remarques importantes
    • XXI-C. Du bon usage des pointeurs
  • XXII. Les pointeurs, ça sert à quoi ?
    • XXII-A. A quoi ça sert?
      • XXII-A-1. Petit rappel.
      • XXII-A-2. Comment faire ?
  • XXIII. Un tableau n'est pas un pointeur ! Vrai ou faux ?
  • XXIV. Passer un tableau à une fonction
    • XXIV-A. Introduction
    • XXIV-B. Tableau à une dimension
    • XXIV-C. Tableau à n dimensions
  • XXV. Retourner un tableau
  • XXVI. char* char**
    • XXVI-A. Introduction
    • XXVI-B. Chaine de caractères
    • XXVI-C. Le type char *
    • XXVI-D. Le type char **
  • XXVII. Initialisation des tableaux de caractères
  • XXVIII. Qu'est-ce qu'une chaine litterale ?
  • XXIX. Bien utiliser malloc()
    • XXIX-A. Suppression du cast
      • XXIX-A-1. Compilateur non ISO
      • XXIX-A-2. Compilateur C++
    • XXIX-B. Déterminer la taille sans le type
  • XXX. Bien utiliser realloc()
  • XXXI. Comment créer un tableau dynamique à deux dimensions ?

XXI. Les pointeurs démythifiés !▲

XXI-A. Introduction▲

Le langage C est indissociable de la notion de pointeur. Ce mot mythique en effraye plus d'un, et il est bon de démythifier enfin les pointeurs, sources de tant d'erreurs de codage, de discussions sans fin et de contre vérités...

XXI-B. Définition▲

Un pointeur est une variable dont la valeur est une adresse.

On distingue deux grandes familles de pointeurs :

  • les pointeurs sur objet ;
  • les pointeurs de fonction.

XXI-B-1. Pointeur sur objet▲

En C, un objet est essentiellement une variable mémoire (par opposition à registre ou constante), modifiable ou non, mais munie d'une adresse.

Un pointeur sur objet (aussi appelé communément 'pointeur') est une variable qui peut contenir l'adresse d'un objet.

Pour définir un pointeur, on utilise un type, puis le signe '*' et enfin l'identificateur, suivit de ';' si on ne désire pas l'initialiser à la déclaration (peu recommandé). Sinon, on utilise l'opérateur '=', suivit de la valeur d'initialisation. Si le pointeur est déclaré dans un bloc, on peut utiliser la valeur retournée par une fonction.

Sélectionnez
/* Definition d'un pointeur sur int */
   int *p_int;

Pour initialiser un pointeur, on peut soit :

  • lui donner la valeur 0 ou NULL, qui signifie "invalide, ne pas utiliser" ;
  • lui donner l'adresse d'une variable ;
  • lui donner la valeur retournée par les fonctions malloc(), realloc(), calloc() ;
  • s'il est de type void ou FILE, lui donner la valeur retournée par fopen() ;
  • s'il est de même type, ou void, lui donner la valeur d'un autre pointeur réputé correctement initialisé.

Pour accéder à la valeur pointée, le pointeur doit être typé (donc différent de void). Dans ce cas, on peut obtenir la valeur en utilisant l'opérateur de déréférencement '*'.

Sélectionnez
   /* Définition de la variable 'a' valant 4 */
   int a = 4;

   /* Définition d'un pointeur 'p' initialisé avec l'adresse de la variable 'a' */
   int *p = &a;

   /* Définition de la variable 'b' non initialisée */
   int b;

   /* Récupération de la valeur de 'a' dans 'b' via le pointeur 'p' */
   b = *p;

   /* 'b' vaut maintenant 4 */

XXI-B-2. Pointeur de fonction▲

Une fonction a une adresse qui est le nom de cette fonction. Un pointeur de fonction peut recevoir cette adresse. Il est possible, via un pointeur de fonction correctement initialisé, d'appeler une fonction.

Cette capacité du langage C lui confère une puissance rarement égalée, qui permet d'écrire du code flexible, dont les adresses des fonctions peuvent être définies à l'exécution. Cela permet de 'personnaliser' des fonctions génériques selon les besoins.

La définition des pointeurs de fonctions est un peu complexe, et a tendance à alourdir le code :

Sélectionnez
   /* pointeur sur une fonction avec deux paramètres */
   int (*pf) (int, char **);

   /* prototype d'un fonction ayant un pointeur de fonction comme paramètre */
   int fun (int (*pf) (int, char **));

Si on doit manipuler des fonctions qui retournent un pointeur de fonction, ou des tableaux de pointeurs de fonction, le code devient rapidement illisible. C'est pourquoi il est fortement conseillé, et ce dans tous les cas, d'utiliser un typedef pour créer un alias sur le type de la fonction.

Sélectionnez
   /* définition d'un alias */
   typedef int fun_f (int, char **);

   /* définition d'un pointeur de fonction de ce type */
   fun_f *pf;

   /* prototype d'une fonction ayant un pointeur de fonction comme paramètre */
   int fun (fun_f *pf);

   /* tableau de pointeurs de fonction */
   fun_f *pf[10];

   /* prototype d'une fonction retournant un pointeur de fonction */
   fun_f *getfunc (int);

La lecture et la maintenance du code s'en trouvent considérablement allégées.

Pour initialiser un pointeur de fonction, on peut soit :

  • lui donner la valeur 0 ou NULL, qui signifie "invalide, ne pas utiliser" ;
  • lui donner l'adresse d'une fonction ;
  • s'il est de même type, lui donner la valeur d'un autre pointeur réputé correctement initialisé.

Pour utiliser le pointeur, il suffit de l'invoquer comme une fonction.

Sélectionnez
   /* définition d'un alias */
   typedef int fun_f (int);

   /* définition d'un pointeur de fonction de ce type */
   fun_f *pf;

   /* prototype d'une fonction du même type */
   int function (int);

   /* NOTA : on peut aussi utiliser le type */
   fun_f function;

   /* initialisation du pointeur de fonction */
   pf = fonction;

   /* appel de la fonction via le pointeur de fonction */
   pf (123);
XXI-B-2-a. Remarques importantes▲
  • void* n'est pas un type correct pour un pointeur de fonction ;
  • Il n'existe pas de type générique pour les pointeurs de fonctions.

XXI-C. Du bon usage des pointeurs▲

Un pointeur est avant tout une variable. Comme toutes les variables, elle doit être initialisée avant d'être utilisée.

Pour un pointeur en général, l'utilisation signifie le passage de sa valeur à une fonction. Pour un pointeur sur objet, c'est le déréférencement par l'opérateur '*'. Pour un pointeur de fonction, c'est l'appel de cette fonction via le pointeur.

Il est recommandé de donner une valeur significative à un pointeur. Soit il est invalide, et on lui donne la valeur 0 ou NULL, soit il est valide, et dans ce cas sa valeur est celle de l'adresse d'un objet ou d'une fonction valide. Si le bloc mémoire ou la fonction deviennent invalides, il est recommandé de donner au pointeur la valeur 0 ou NULL.

Sélectionnez
   /* Définition du pointeur. Il est initialisé à l'état invalide */
   int *p = NULL;

   /* le pointeur est initialisé avec l'adresse d'un tableau
    * dynamique de 4 éléments
    */
   p = malloc (4 * sizeof *p);

   /* en cas d'échec d'allocation, malloc() retourne NULL */
   if (p != NULL)
   {
      /* ... */

      /* après utilisation, l'espace mémoire est libéré */
      free (p);

      /* le pointeur est forcé à l'état invalide */
      p = NULL;
   }

XXII. Les pointeurs, ça sert à quoi ?▲

Les pointeurs sont un peu l'essence même du C et de tous les autres langages. Sauf que dans beaucoup de langages, cette notion est considérée comme honteuse (ou trop technique), et des astuces sont utilisées pour 'cacher' les pointeurs.

En C, on n'a pas honte des pointeurs et on les affiche ostensiblement.

XXII-A. A quoi ça sert?▲

XXII-A-1. Petit rappel.▲

Un paramètre de fonction est une variable locale de la fonction dont la valeur est donnée par l'appelant. Si on modifie cette valeur, on ne fait que modifier une variable locale. Exemple :

Sélectionnez
{
   int x = 123;
   f (x);
}

avec :

Sélectionnez
void f (int a)
{
   a++;
}

Déroulement des opérations :

  • x prend la valeur 123 ;
  • appel de la fonction f() avec en paramètre la valeur de x (soit 123) ;
  • dans f(), la variable locale a prend la valeur 123 ;
  • la valeur de a est augmentée de 1 et devient 124 ;
  • la fonction se termine ;
  • la valeur de x n'a pas changé, elle vaut toujours 123.

Si on avait voulu modifier x en appelant f(), c'est raté.

XXII-A-2. Comment faire ?▲

Tout simplement en donnant un moyen à la fonction qui lui permettre de modifier la variable x. Ce moyen est simple. Il suffit de lui donner l'adresse de x et elle pourra alors modifier x grâce à un pointeur et à l'opérateur de déréférencement (*).

En fait, on va faire ceci :

Sélectionnez

{
   int x = 123;
   int *p = &x;
   (*p)++;
}

C'est à dire

  • x prend la valeur 123 ;
  • p prend l'adresse de x ;
  • x est incrémenté via p et l'opérateur de déréférencement. Il vaut maintenant 124.

sauf que les opérations vont être réparties entre l'appelant et l'appelé comme ceci :

Sélectionnez
{
   int x = 123;
   f (&x);
}

avec

Sélectionnez
void f (int *p)
{
   (*p)++;
}
  • x prend la valeur 123 ;
  • l'adresse de x est passée à la fonction ;
  • dans f(), la variable locale p est initialisée avec l'adresse de x ;
  • la valeur de x est incrémentée de 1 via p et l'opérateur de déréférencement. Elle vaut maintenant 124 ;
  • la fonction se termine ;
  • la valeur de x a changé, elle vaut maintenant 124.

Voilà donc un exemple d'utilisation des pointeurs. Pour un tableau, par exemple, il n'y a pas le choix. La seule façon de faire est de passer l'adresse du premier élément du tableau via un pointeur sur le même type que l'élément.

Ensuite, on a accès à tous les éléments du tableau. Non seulement le [0] en *p , mais aussi aux autres, grâce aux propriétés de l'arithmétique des pointeurs.

En effet, le type étant connu, l'adresse des autres éléments est tout simplement p+1, p+2 etc.

L'élément lui même se trouve donc en *(p + 1), *(p + 2) etc. Le langage C définit cette écriture comme strictement équivalente à p[1], p[2] etc. On dit alors que la 'notation tableau' peut s'appliquer aux pointeurs. Mais cela ne signifie pas qu'un pointeur soit un tableau ni inversement comme on le lit parfois.

Ce principe est massivement utilisé avec les chaînes de caractères, qui, rappelons le, sont des tableaux de char initialisés avec des valeurs de caractères et terminés par un 0.

XXIII. Un tableau n'est pas un pointeur ! Vrai ou faux ?▲

Le tableau est probablement le concept le plus difficile à définir correctement en C. Il y a en effet beaucoup de confusion sur les termes : tableau, adresse, pointeur, indices... Ce petit article essaye de tirer les choses au clair.

  • un tableau est une séquence d'éléments de types identiques ;
  • le nom du tableau est invariant. Il a la valeur et le type de l'adresse du premier élément du tableau. C'est donc une adresse typée (encore appelée pointeur constant[1]). Étant de la même nature qu'un pointeur, les même règles d'adressage s'appliquent, à savoir que le premier élément est en tab, soit tab + 0 et que son contenu est donc *(tab + 0). De même le contenu du deuxième élément est *(tab + 1) etc. ;
  • cette syntaxe étant un peu lourde, le langage C définit une simplification qui est tab[0], tab[1] etc. Le nombre entre les crochets est appelé indice. Son domaine de définition pour un tableau de taille N est 0 à N-1.

représentation graphique d'un tableau de 4 éléments :

Sélectionnez
tab    : |---------------|
tab[0] : |---|
tab[1] :     |---|
tab[2] :         |---|
tab[3] :             |---|

[1] C'est là que ce situe la difficulté. Le langage C parle de non-modifiable L-value ce qui signifie que c'est un objet (il a une adresse), non modifiable. On ne peut pas changer sa valeur. On ne peut changer que la valeur de ses éléments.

XXIV. Passer un tableau à une fonction▲

XXIV-A. Introduction▲

Rappel : en langage C, les passages de paramètres se font exclusivement par valeur.

Le langage C n'autorise pas le passage d'un tableau en paramètre à une fonction. La raison est probablement une recherche d'efficacité, afin d'éviter des copies inutiles.

Le but de 'passer un tableau' à une fonction est en fait de permettre à celle-ci d'accéder aux éléments du tableau en lecture ou en écriture. Pour se faire, l'adresse du début du tableau et le type des éléments suffisent à mettre en œuvre l'arithmétique des pointeurs. Un paramètre 'pointeur' est donc exactement ce qu'il faut.

XXIV-B. Tableau à une dimension▲

Soit l'appelant :

Sélectionnez
int main (void)
{
   int tab[5];

   clear (tab);

   return 0;
}

Le prototype de la fonction appelée doit comporter un pointeur du même type que les éléments du tableau, pour recevoir l'adresse du premier élément de celui-ci, soit :

Sélectionnez
void clear (int *p);

La fonction va donc utiliser le paramètre pointeur, dont la valeur est l'adresse du premier élément du tableau, pour accéder aux éléments du tableau. Par exemple, les mettre à 0 :

Sélectionnez
void clear (int *p)
{
   *(p + 0) = 0; /* premier élément, */
   *(p + 1) = 0; /* deuxième élément, */
   *(p + 2) = 0; /* troisième élément, */
   *(p + 3) = 0; /* quatrième élément, */
   *(p + 4) = 0; /* dernier élément (cinquième) */
}

Afin d'alléger l'écriture, le langage C autorise l'utilisation de la syntaxe des tableaux pour accéder aux éléments :

Sélectionnez
void clear (int *p)
{
   p[0] = 0; /* premier élément, */
   p[1] = 0; /* deuxième élément, */
   p[2] = 0; /* troisième élément, */
   p[3] = 0; /* quatrième élément, */
   p[4] = 0; /* dernier élément (cinquième) */
}

Cette implémentation est évidemment théorique, car dans la pratique, on utilisera une boucle et un paramètre supplémentaire (nombre d'éléments) afin d'écrire un code plus souple et auto adaptatif.

Sélectionnez
void clear (int *p, size_t nb)
{
   size_t i;

   for (i = 0; i < nb; i++)
   {
      p[i] = 0;
   }
}

int main (void)
{
   int tab[5];

   clear (tab, 5);

   return 0;
}

XXIV-C. Tableau à n dimensions▲

Rappelons que lorsqu'on définit un paramètre, les syntaxes type *param et type param[] sont sémantiquement équivalentes.

Pour définir un paramètre de type pointeur sur un tableau à deux dimensions, on serait tenté d'écrire type p[][], ce qui serait une erreur de syntaxe. En effet, la notation [] est une notation abrégée de [TAILLE] dans les cas où cette taille est ignorée par le compilateur, c'est à dire lorsque la dimension concernée est la plus à gauche. Les syntaxes suivantes sont légales :

Sélectionnez
type_retour fonction (int p[])
type_retour fonction (int p[12])
type_retour fonction (int p[][34])
type_retour fonction (int p[56][78])
etc.

Pour déterminer le nombre d'éléments d'un tableau, on peut utiliser les propriétés des tableaux. En effet, un tableau est une suite contiguë d'éléments identiques. Le nombre d'éléments est donc tout simplement le rapport entre la taille en bytes du tableau (sizeof tab) et la taille d'un élément en bytes (sizeof tab[0] ou sizeof *tab), que l'on généralise sous la forme d'une macro bien connue :

Sélectionnez
#define NB_ELEM(a) (sizeof (a) / sizeof *(a))

XXV. Retourner un tableau▲

En C une fonction ne sait pas 'retourner un tableau'.

Ce qu'elle sait faire, c'est retourner une valeur. La pratique courante est de retourner l'adresse du premier élément du tableau. Pour cela, on définit le type retourné comme un pointeur sur le type d'un élément du tableau.

Sélectionnez
T *f();

T représente le type d'un élément du tableau

Évidemment, cette adresse doit être valide après exécution de la fonction. Même si c'est techniquement possible, il est donc hors de question de retourner l'adresse d'un élément appartenant à un tableau local.

  • soit on passe à la fonction l'adresse du premier élément d'un tableau existant, et elle peut retourner l'adresse de ce tableau ;
  • soit la fonction fait une allocation dynamique et retourne l'adresse du bloc alloué ;
  • on peut aussi retourner l'adresse du premier élément d'une chaîne littérale. Celle-ci est statique (attention accès en lecture seule, qualificateur 'const' recommandé) ;
  • enfin, il est techniquement possible de retourner l'adresse du premier élément d'un tableau statique, mais cette pratique est déconseillée, car elle rend la fonction non ré-entrante, donc impropre à plusieurs utilisations comme les appels imbriqués ou les threads... Plusieurs fonctions du C sont malheureusement victimes de ce défaut (ctime() asctime(), strtok() etc.)

XXVI. char* char**▲

XXVI-A. Introduction▲

En langage C, beaucoup de problèmes de codage et d'exécution proviennent d'une confusion entre tableaux et pointeurs. C'est particulièrement vrai avec les chaînes de caractères, au point d'utiliser le terme 'Char étoile' à la place du terme 'Chaîne de caractères' ou 'Char étoile étoile' à la place de 'Tableau de chaînes'. Qu'en est-il exactement ?

XXVI-B. Chaine de caractères▲

Une chaîne de caractères est un tableau de char terminé par un 0. Une chaîne littérale n'est pas modifiable.

Sélectionnez
     /* interdit */
   "hello"[2] = 'x';

   /* autorise' */
   char s[] = "hello";

   s[2] = 'x';

XXVI-C. Le type char *▲

Un pointeur sur char est une variable qui peut contenir NULL, l'adresse d'un char ou celle d'un élément d'un tableau de char. Si c'est un tableau, on n'a aucune information sur le nombre d'éléments du tableau pointé. Néanmoins, si c'est une chaîne valide, elle est terminée par un 0 qui sert de balise de fin.

Sélectionnez
char c;
   char *pa = &c;

   char *pb = 0;
   char *pc = NULL;

   char s[] = "hello";

   char *pd = s;
   char *pe = s + 3;

   /* une chaîne littérale n'étant pas modifiable,
    * il est conseillé de qualifier l'objet avec
    * 'const' (read-only)
    */
   char const *pf = "hello";
   char const *pg = "hello" + 2;

Un petit schéma pour modéliser :

Représentation graphique d'un objet 'c' de type char non initialisé :

Sélectionnez
char c;

   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : ???    :
   :---------:--------:

Représentation graphique d'un objet 'c' de type char après initialisation :

Sélectionnez
c = 'A';

   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : 'A'    :
   :---------:--------:

Représentation graphique d'un objet 'p' de type char * non initialisé :

Sélectionnez
char *p;

   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : ???    : ???     :
   :---------:--------:---------:

Représentation graphique d'un objet 'p' de type char * après initialisation (NULL) :

Sélectionnez
p = NULL;

   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : NULL   : ???     : --> NULL
   :---------:--------:---------:

Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une variable) :

Sélectionnez
p = &c;

   :---------:--------:---------:     :---------:--------:
   : adresse : valeur : valeur  :     : adresse : valeur :
   :         :        : pointée :     :         :        :
   :---------:--------:---------:     :---------:--------:
   : &p      : &c     : 'A'     : --> : &c      : 'A'    :
   :---------:--------:---------:     :---------:--------:

Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une chaîne modifiable) :

Sélectionnez
char s[]="ab";
   p = s;

   :---------:---------:---------:     :---------:--------:
   : adresse : valeur  : valeur  :     : adresse : valeur :
   :         :         : pointée :     :         :        :
   :---------:---------:---------:     :---------:--------:
   : &p      : &s[0]   : 'a'     : --> : s+0     : 'a'    :
   :         : ou s+0  :         :     :---------:--------:
   :         : ou s    :         :     : s+1     : 'b'    :
   :---------:---------:---------:     :---------:--------:
                                       : s+2     : 0      :
                                       :---------:--------:

Le pointeur sur char est principalement utilisé pour les paramètres 'chaînes de caractères' des fonctions et pour des manipulations de chaînes de caractères.

Mais cela n'autorise pas à utiliser le terme 'char étoile' à la place de 'chaîne de caractères'.

XXVI-D. Le type char **▲

Un pointeur sur pointeur de char est une variable qui peut contenir NULL, l'adresse d'un pointeur de char ou celle d'un élément d'un tableau de pointeurs de char. Si c'est un tableau, on a aucune information sur le nombre d'éléments du tableau pointé. On peut parfois ajouter un élément de valeur NULL pour délimiter le tableau de pointeurs. Un petit schéma pour modéliser :

XXVII. Initialisation des tableaux de caractères▲

Il est possible d'initialiser un tableau de char au moment de sa définition avec une constante qui ressemble a une chaîne de caractères. Il faut cependant faire attention, car il n'est pas garanti que le tableau ainsi initialisé forme une chaîne C valide (c'est à dire terminée par un 0).

En effet, la liste de caractères placée entre double quotes et servant à initialiser le tableau de char n'est en aucun cas une chaîne de caractères. C'est une simple liste de caractères. Les zéros que l'on voit dans le tableau sont le résultat du comportement standard du C qui complète à 0 les tableaux partiellement initialisés.

Exemples :

Sélectionnez
char s[4] = "ab";

le tableau est initialisé avec {'a', 'b', 0, 0} : Chaîne valide

Sélectionnez
char s[4] = "abc";

le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaîne valide

Sélectionnez

char s[4] = "abcd";

le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaîne invalide /!\

Sélectionnez
char s[4] = "abcde";

Ne compile pas (trop d'initialisateurs)

XXVIII. Qu'est-ce qu'une chaine litterale ?▲

Une chaîne littérale, telle qu'elle apparaît dans un source C, est une séquence de caractères entourée de guillemets (double quotes)

Sélectionnez
"hello"

Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char, par exemple :

Sélectionnez
char s[] = "hello";

(les détails sont indiqués ici).

Une chaîne littérale désigne en réalité l'adresse du premier élément d'un tableau de char anonyme non modifiable, situé en mémoire statique, initialisé avec la séquence de caractères mentionnés et terminé par un 0.

Tout se passe comme si on avait ceci :

Sélectionnez
static char const identificateur_connu_seulement_du_compilateur[] = {'h','e','l','l','o',0};

Si la chaîne apparaît dans un paramètre de fonction :

Sélectionnez
f ("hello");

c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :

Sélectionnez
void f (char const *s);

Si la chaîne sert à initialiser un pointeur :

Sélectionnez
char const *p = "hello";

p = "bye";

c'est cette valeur (l'adresse) qui est stockée dans le pointeur.

Rappel : le mot clé const sert à qualifier l'objet de non modifiable

XXIX. Bien utiliser malloc()▲

Il est fréquent de rencontrer ce genre de code :

Sélectionnez
#include <stdlib.h>
...
{
   /* création d'un tableau de 10 int */
   size_t n = 10;
   int *p = (int*) malloc (sizeof (int) * n);

   if (p != NULL)
   {
      ...
}

Ce code est correct et ne présente pas de comportement indéterminé. Cependant, il est inutilement compliqué et peut être amélioré de plusieurs façons.

XXIX-A. Suppression du cast▲

Il est d'usage d'éviter les casts en C. Certains sont indispensables, d'autres non. Ici, par exemple, et contrairement aux idées reçues, le cast est inutile, et on peut parfaitement écrire :

Sélectionnez
{
   int *p = malloc (sizeof (int) * n);
}

Il est cependant des cas rares où le cast est indispensable :

  • le compilateur n'est pas conforme à ISO C-90 ou ISO C-99 ;
  • le compilateur n'est pas C mais par exemple pré-C++98.

XXIX-A-1. Compilateur non ISO▲

Il est rare de nos jours d'utiliser un compilateur datant d'avant la normalisation du langage C (1989 aux USA, 1990 au niveau international). En effet, ces compilateurs ne supportent pas les prototypes, ce qui les rend impropre à produire du code cohérent, à moins d'utiliser un outil de vérification indépendant comme PCLint.

Le cas peut cependant se produire, s'il s'agit de maintenir du code ancien avec une chaîne de compilation ancienne. Dans ce cas, effectivement, le cast est indispensable si le type du pointeur et différent de char*.

L'opportunité de conserver une telle pratique est donc laissée à l'appréciation du programmeur. Il semble cependant assez évident que dans les nouveaux développements utilisant un compilateur ISO, il est inutile d'ajouter le cast.

XXIX-A-2. Compilateur C++▲

Il est techniquement possible, à de rares exceptions syntaxiques près, de faire compiler du code C par un compilateur C++. Néanmoins, cette pratique est rarement justifiée et est largement déconseillée.

En effet, en dehors des points syntaxiques évidents (comme par exemple la conversion de type explicite void* <-> type* qui justement oblige à utiliser le cast) plusieurs points de sémantique diffèrent entre les deux langages. En l'état actuel des normes, les spécifications C++98 et C99 ont même plutôt tendance à diverger (cette situation pourrait changer en 2005 avec une nouvelle révision de C++ intégrant les nouveautés de C99).

On peut aussi se demander pourquoi on utiliserait malloc() et free() en C++, alors que ce langage dispose des opérateurs new et delete . D'autre part, en C++98, un cast se fait avec static_cast<...>.

Lire à ce sujet : Incompatibilities Between ISO C and ISO C++

XXIX-B. Déterminer la taille sans le type▲

Il est courant de déterminer la taille d'un objet en utilisant son type

Sélectionnez
{
   int *p = malloc (sizeof (int));
}

Si le type change, on est obligé de modifier deux fois le code :

Sélectionnez
{
   long *p = malloc (sizeof (long));
}

Lorsqu'il s'agit d'un pointeur typé, il existe une technique alternative qui consiste à utiliser la taille d'un élément pointé par ce pointeur :

Sélectionnez
{
   int *p = malloc (sizeof *p);
}

Le changement de type se trouve largement simplifié :

Sélectionnez
{
   long *p = malloc (sizeof *p);
}

Quelques compléments sur malloc() et l'allocation dynamique en général dans l'article Description des mécanismes d'allocation dynamique de mémoire en langage C

XXX. Bien utiliser realloc()▲

La fonction realloc(), bien que souvent décriée pour sa lenteur, offre une alternative intéressante pour gérer des tableaux de taille variable. Bien sûr il ne faut pas allouer les objets un par un, mais par blocs (doublage, par exemple).

Pour utiliser correctement realloc(), quelques précautions doivent être prise. Par exemple :

Sélectionnez
#include <stdlib.h>

<...>
{
   /* allocation d'un tableau de 10 int
    * Pour pouvoir gérer la taille, on utilise
    * une variable 'taille'. Une structure comprenant
    * l'adresse du tableau et sa taille est aussi envisageable.
    */

   size_t size = 10;
   int *p = malloc (size * sizeof *p);

<...>

   /* Agrandissement du tableau a 15 int */
   {
      /* reallocation. Le résultat est stocke'
       * dans une variable temporaire
       */
      size = 15;
      type_s *p_tmp = realloc (p, size * sizeof *p_tmp);

      if (p_tmp != NULL)
      {
         /* si la nouvelle valeur est valide,
          * le pointeur original est mis a jour.
          */
         p = p_tmp;
      }
      else
      {
         /* l'ancien bloc est valide, mais il n'a pas été agrandi */
      }
   }
}

Il faut aussi garder à l'esprit que la partie nouvellement allouée n'est pas initialisée.

XXXI. Comment créer un tableau dynamique à deux dimensions ?▲

Il y a plusieurs façons de créer un tableau dynamique à deux dimensions de type T. La plus courante consiste à créer un tableau de N lignes contenant les adresses des N tableaux de M colonnes. L'avantage de cette méthode est qu'elle permet un usage habituel du tableau avec la notation [i][j].

Comme le tableau de N lignes contient des adresses, ses éléments sont donc des pointeurs sur T. Il se définit ainsi :

Sélectionnez
T* *pp = malloc (sizeof (T*) * N);

T représente le type d'un élément du tableau

Ensuite, chaque élément reçoit l'adresse du premier élément d'un tableau alloué de M colonnes. Chaque élément est donc de type T :

Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
      pp[i] = malloc (sizeof (T) * M);
   }

Bien sûr, pour une utilisation correcte, il faut en plus tenir compte du fait que malloc() peut échouer et qu'il faut libérer les blocs alloués après usage.

D'autre part, je rappelle que les valeurs d'un bloc fraîchement alloué sont indéfinies.

Enfin, selon les principes énoncés ici, on peut simplifier le codage comme ceci :

Sélectionnez
T **pp = malloc (sizeof *pp * N);
Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
      pp[i] = malloc (sizeof *pp[i] * M);
   }

ce qui facilite la maintenance et évite bien des erreurs de type, le choix étant confié au compilateur.

Il va sans dire qu'il faut ensuite libérer le tableau alloué selon le procédé inverse :

Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
     free(pp[i]), pp[i] = NULL;
   }
   free(pp), pp = NULL;

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XXXII. Saisie de données par un opérateur (stdin)
    • XXXII-A. Introduction
    • XXXII-B. fgetc(), getc(), getchar()
    • XXXII-C. gets()
    • XXXII-D. scanf()
    • XXXII-E. fgets()
    • XXXII-F. Ressources
    • XXXII-G. Comment fonctionne fgetc(stdin) alias getchar()
      • XXXII-G-1. Comportement visible.
      • XXXII-G-2. Comportement interne.
      • XXXII-G-3. Quelques expérimentations.
  • XXXIII. Les fichiers
    • XXXIII-A. Introduction
    • XXXIII-B. Texte ou binaire ?
      • XXXIII-B-1. Fichier texte
      • XXXIII-B-2. Fichier binaire
      • XXXIII-B-3. Modes d'ouverture d'un fichier
      • XXXIII-B-4. Lecture d'un fichier
        • XXXIII-B-4-a. fgetc(), getc()
        • XXXIII-B-4-b. fread()
        • XXXIII-B-4-c. fscanf()
        • XXXIII-B-4-d. fgets()
          • XXXIII-B-4-d-i. Exemple d'utilisation
          • XXXIII-B-4-d-ii. Explication
          • XXXIII-B-4-d-iii. Critique de cet exemple
          • XXXIII-B-4-d-iv. Détection d'une erreur
          • XXXIII-B-4-d-v. Gestion des fins de ligne
          • XXXIII-B-4-d-vi. Exemple amélioré avec détection de la fin de lecture
          • XXXIII-B-4-d-vii. Explication
      • XXXIII-B-5. Écriture dans un fichier
        • XXXIII-B-5-a. fputc(), putc()
        • XXXIII-B-5-b. fwrite()
        • XXXIII-B-5-c. fprintf()
        • XXXIII-B-5-d. fputs()
      • XXXIII-B-6. Bien utiliser les formats de données
        • XXXIII-B-6-a. Format orienté texte
        • XXXIII-B-6-b. Format orienté binaire
    • XXXIII-C. Supprimer un enregistrement dans un fichier binaire
    • XXXIII-D. En guise de conclusion
  • XXXIV. Pourquoi fflush (stdout) ?

XXXII. Saisie de données par un opérateur (stdin)▲

XXXII-A. Introduction▲

Il est courant en C standard d'utiliser le flux stdin pour acquérir des données en provenance d'un opérateur. (Mode conversationnel). On admettra pour la suite que stdin est connecté à la partie 'clavier' d'un périphérique console.

Le langage C offre plusieurs fonctions permettant de lire des données sur un flux en général et sur stdin en particulier :

  • fgetc() ;
  • getc() ;
  • getchar() ;
  • gets() ;
  • scanf() ;
  • fgets().

XXXII-B. fgetc(), getc(), getchar()▲

Ces trois fonctions extraient un caractère du flux entrant (pour getchar(), ce flux est stdin). C'est insuffisant pour saisir autre chose qu'un simple <ENTER>. Ces fonctions ne sont absolument pas adaptées à la saisie d'un caractère comme un choix de menu par exemple.

Par contre, ces fonctions peuvent être utilisées pour construire des fonctions d'entrées de plus haut niveau plus ou moins spécialisées.

Détails de fonctionnement de fgetc()

XXXII-C. gets()▲

Pour des raisons évidentes de sécurité (pas de limitation du nombre de caractères saisis), la fonction gets() ne devrait pas être utilisée. Bien que, à ma connaissance, cette fonction ne soit pas officiellement dépréciée pour des raisons de compatibilité avec le code existant, il est fortement conseillé de ne pas l'utiliser pour de nouveaux développements.

XXXII-D. scanf()▲

Malgré ce que l'on constate dans l'abondante littérature consacrée à l'initiation au langage C, l'utilisation de scanf() n'est pas adaptée.

En effet, le 'f' de scanf() est là pour nous rappeler que l'entrée doit être formatée (formated), ce qui n'est évidemment pas le cas avec un opérateur humain qui peut entrer n'importe quoi. D'autre part, scanf() gère difficilement le '\n', ce qui entraîne des comportements aberrants dans les saisies si on ne prend pas certaines précautions d'usage.

L'utilisation correcte et sûre de scanf() est complexe, et n'est pas à la porté d'un débutant (ni même à celle de la plupart des programmeurs expérimentés). Néanmoins, il est possible d'utiliser correctement scanf() si on se forme correctement. En lisant par exemple l'article Scanf démythifiée.

XXXII-E. fgets()▲

Cette fonction est parfaitement adaptée à la saisie d'une ligne, (même de 1 caractère). Son usage est recommandé.

S'il faut saisir une valeur numérique, celle-ci sera d'abord saisie sous forme de ligne, puis traduite par la fonction appropriée (strtol(), strtoul(), strtod()) ou sscanf()) avec le filtre approprié :

Sélectionnez
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
   int ret;
   char temp[20];

   do
   {
      char saisie[20];

      printf("Entrez un nombre : ");
      fflush (stdout);

      fgets (saisie, sizeof saisie, stdin);

      /* Filtrage des caractères (entier décimal)
       * Nota : la saisie s'arrête à la première erreur.
       * Ce qui est saisi avant est considéré comme valide.
       *
       * "123a" -> "123" : ret = 1
       *
       * "a123" -> ""    : ret = 0
       */
      ret = sscanf (saisie, "%[0-9-]s", temp);
   }
   while (ret != 1);

   {
      long n = strtol (temp, NULL, 10);

      printf ("La chaîne est '%s', soit %ld\n", temp, n);
   }
   return 0;
}

D'autres exemples dans le chapitre sur les fichiers.

XXXII-F. Ressources▲

Comment se fabriquer des entrées solides en C

XXXII-G. Comment fonctionne fgetc(stdin) alias getchar()▲

Cette fonction d'apparence simple a en fait un comportement plus complexe qu'il n'y parait. En effet, elle regroupe un certain nombre de comportements non triviaux qui sont rarement expliqués dans la littérature C.

XXXII-G-1. Comportement visible.▲

L'appel de cette fonction provoque une suspension de l'exécution du programme. Durant cette suspension, il est possible de rentrer des caractères (par exemple à l'aide du clavier) et même éventuellement de supprimer le ou les derniers caractères saisis à l'aide de la touche 'BackSpace'. La fin de saisie (et la reprise de l'exécution du programme) est marquée par la frappe de la touche <enter>.

XXXII-G-2. Comportement interne.▲

Les caractères saisis sont stockés dans le flux stdin. Lorsque l'on frappe la touche <enter>, le caractère '\n' est aussi placé dans stdin, et l'exécution reprend. Le caractère le plus ancien est alors extrait du flux et il est retourné. En cas d'erreur de lecture ou d'entrée d'un caractère spécial dit 'de fin de fichier' (Ctrl-D, Ctrl-Z etc. selon le système), la valeur EOF (int < 0) est retournée.

Ensuite, si on rappelle fgetc(), deux cas sont possibles. Soit le flux est vide, soit il ne l'est pas. Si le flux est vide, la fonction fgetc() suspend l'exécution, et on retrouve le comportement précédent. S'il n'est pas vide, l'exécution n'est pas suspendue, et le caractère le plus ancien est extrait et retourné.

Dans la grande majorité des cas la lecture du '\n' signifie que la ligne saisie a été complètement lue.

XXXII-G-3. Quelques expérimentations.▲

A l'aide de simples programmes, il est possible de vérifier un certain nombre de comportements décrits précédemment :

Sélectionnez
#include <stdio.h>

int main (void)
{

   int x = fgetc(stdin);

   printf ("x = %d ('%c')\n", x, x);

   return 0;
}

Quelques essais de saisie :

Sélectionnez
<enter>
x = 10 ('
')

On voit que le caractère extrait est '\n' (ici, LF, soit le code ASCII 10)

Sélectionnez
a<enter>
x = 97 ('a')

On voit que le caractère extrait est 'a' (ici, le code ASCII 97). Le <enter> ('\n') n'a pas été extrait. Si on appelait fgetc() une nouvelle fois, il n'y aurait pas de suspension.

Sélectionnez
a<backspace>b<enter>
x = 98 ('b')

On constate que, bien que le premier caractère saisi fut 'a', le caractère extrait est 'b' (ici, le code ASCII 98). En effet, la touche <backspace> a permis de corriger la dernière saisie.

Sélectionnez
abcd<enter>
x = 97 ('a')

On voit que le caractère extrait est 'a', bien que d'autres caractères aient été saisis après. C'est donc bien le plus ancien caractère qui est extrait. Les autres caractères sont en attente de lecture. Une boucle de fgetc() permettrait de les extraire.

Sélectionnez
#include <stdio.h>

int main (void)
{
   int x;

   do
   {
      x = fgetc(stdin);
      printf ("x = %d ('%c')\n", x, x);
   }
   while (1);

   return 0;
}

Je laisse au lecteur le soin de refaire les expériences précédentes et d'en tirer les conclusions qui s'imposent.

XXXIII. Les fichiers▲

XXXIII-A. Introduction▲

Le langage C n'offre pas, à proprement parler, de gestion de fichiers. Il définit plutôt des flux d'entrées / sorties (I/O streams) sur lesquels il peut agir (ouverture/fermeture, lecture/écriture). L'unité d'information gérée par un flux est le byte.

Certains de ces flux sont connectés à des périphériques permettant par exemple de réaliser une interface entre la machine et l'utilisateur (IHM) en mode texte. Mais la plupart du temps, le nom associé au flux est en fait un 'fichier', c'est-à-dire une sorte de mémoire (disque, flash) accessible en écriture et en lecture par l'intermédiaire du système. L'avantage évident est que les données sont permanentes, même après mise hors tension de la machine.

En conséquence, dans la pratique, les termes flux et fichiers sont souvent confondus.

XXXIII-B. Texte ou binaire ?▲

Le langage C fait la distinction entre les fichiers binaires et les fichiers textes. Cette distinction est historique. Elle dépend en fait du système utilisé. Sur certains systèmes, il n'existe aucune différence physique entre les fichiers textes et les fichiers binaires. Sur d'autres systèmes, il existe une différence. Par souci de portabilité, il est recommandé de respecter cette distinction.

Le choix entre fichier texte ou binaire provient du contenu de ce fichier.

XXXIII-B-1. Fichier texte▲

On appelle fichier texte un fichier qui contient des informations de type texte, c'est à dire des séquences de lignes.

Une ligne est une séquence de caractères imprimables terminée par une marque de fin de ligne.

Selon le système, la marque de fin de ligne est composée de un ou plusieurs caractères de contrôle (par exemple, CR, LF, ou une séquence de ces caractères)

Sélectionnez
:----------------:--------------:----------------:
: Système        : Fin de ligne : Fin de fichier :
:----------------:--------------:----------------:
: Unix           :              :                :
: Mac X          : 0x0A LF      : Sans objet     :
: Linux          :              :                :
:----------------:--------------:----------------:
: Mac (non unix) : 0x0D CR      : Sans objet     :
:----------------:--------------:----------------:
: MS-DOS         : 0x0D CR      : 0x1A           :
: Windows        : 0x0A LF      : ^Z             :
: Windows NT     :              :                :
:----------------:--------------:----------------:
: VMS STREAM_CR  : 0x0D CR      : Sans objet     :
:----------------:--------------:----------------:
: VMS STREAM_LF  : 0x0A LF      : Sans objet     :
:----------------:--------------:----------------:
: VMS STREAM_CRLF: 0x0D CR      : Sans objet     :
:                : 0x0A LF      :                :
:----------------:--------------:----------------:

L'ensemble des valeurs numériques des caractères (charset) dépend du système. La plupart du temps, il s'agit du codage ASCII (0-127) avec des extensions plus ou moins standards au delà de 127. Il existe d'autres codes, comme EBCDIC utilisé sur certains mainframes IBM.

Pour écrire une fin de ligne dans un fichier texte, il suffit d'écrire le caractère '\n'. Celui-ci sera alors automatiquement traduit en marqueur de fin de ligne.

De même, lors de la lecture d'un fichier texte, le marqueur de fin de ligne est automatiquement traduit en '\n', quel qu'il soit.

Nota : Certains systèmes marquent la fin des fichiers textes d'un caractère spécial. Par exemple MS-DOS ajoute un code 26 (^Z). Cela signifie que, pour ce système, la lecture d'un fichier texte s'arrête dès la rencontre de ce caractère.

XXXIII-B-2. Fichier binaire▲

N'importe quel fichier, y compris un fichier texte, peut être considéré comme binaire. Dans ce cas, l'écriture et la lecture des caractères se fait sans interprétation.

Par exemple, sur une plate-forme utilisant le jeu de caractères ASCII, CR vaut 13 ou 0x0D ou '\r'. De même, LF vaut 10 ou 0x0A ou '\n'.

XXXIII-B-3. Modes d'ouverture d'un fichier▲

La fonction d'ouverture de fichier est fopen(). Comme pour les autres fonctions de gestion des fichiers, le fichier d'interface est <stdio.h>.

Sélectionnez
FILE *fopen (char const *filename, char const *mode);

Le mode d'ouverture est déterminé par une chaîne de caractère. Voici les chaînes correspondant aux principaux modes :

Sélectionnez
   "r"  : mode texte en lecture
   "w"  : mode texte en écriture (création)
   "a"  : mode texte en écriture (ajout)

   "rb" : mode binaire en lecture
   "wb" : mode binaire en écriture (création)
   "ab" : mode binaire en écriture (ajout)

XXXIII-B-4. Lecture d'un fichier▲

Le langage C offre plusieurs fonctions permettant de lire les données d'un fichier :

  • fgetc() ;
  • getc() ;
  • fread() ;
  • fscanf() ;
  • fgets().
XXXIII-B-4-a. fgetc(), getc()▲

Ces fonctions sont identiques. Elles permettent de lire un caractère.

XXXIII-B-4-b. fread()▲

Cette fonction permet de lire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à la lecture des données binaires brutes (non interprétées).

XXXIII-B-4-c. fscanf()▲

Cette fonction permet de lire des données 'texte' formatées. Cette fonction est d'une utilisation complexe et son usage est peu recommandé.

XXXIII-B-4-d. fgets()▲

Cette fonction permet de lire une ligne de texte. Elle est tout à fait adaptée à la lecture d'un fichier texte ligne par ligne.

Sa simplicité d'utilisation et sa robustesse en font la fonction préférée des programmeurs qui doivent analyser des fichiers textes.

XXXIII-B-4-d-i. Exemple d'utilisation▲

Soit le fichier texte :

Sélectionnez
Ceci est un simple fichier
texte de 2 lignes.

et un petit programme permettant de lire ces deux lignes :

Sélectionnez
/* fichier1.c */
#include <stdio.h>

int main (void)
{
   /* ouverture du fichier en mode texte */
   FILE *fp = fopen ("data.txt", "r");

   /* L'ouverture du fichier est-elle réalisée ? */
   if (fp != NULL)
   {
      /* définition d'un tableau de char destine a recevoir la ligne
       * La taille est arbitraire. Elle doit être cependant adaptée * aux besoins courants.
       * Pour les grandes tailles (disons > 256 char),
       * il est préférable d'utiliser une allocation dynamique.
       */
      char ligne[32];

      /* lecture de la première ligne */
      fgets (ligne, sizeof ligne, fp);

      /* Affichage de la première ligne */
      printf ("1: %s\n", ligne);

      /* lecture de la deuxième ligne */
      fgets (ligne, sizeof ligne, fp);

      /* Affichage de la deuxième ligne */
      printf ("2: %s\n", ligne);

      /* Fermeture du fichier */
      fclose (fp);
   }
   else
   {
      printf ("Erreur d'ouverture du fichier\n");
   }
   return 0;
}

On doit obtenir ceci sur la sortie standard (stdout) :

Sélectionnez
1: Ceci est un simple fichier

2: texte de 2 lignes.
XXXIII-B-4-d-ii. Explication▲

La ligne lue est stockée dans la variable ligne, y compris le '\n'. La fonction d'affichage printf() affiche le numéro de ligne, suivit de ': ', la ligne (avec son '\n') et un '\n' en plus, ce qui explique la présence de lignes "vides".

XXXIII-B-4-d-iii. Critique de cet exemple▲

Cet exemple de codage 'naïf' souffre d'un défaut majeur : Il fait l'hypothèse que le fichier fait 2 lignes, et il continue à lire le fichier même si une erreur de lecture s'est produite. En fait, tout simplement, il ne gère pas les erreurs de lecture.

Il est facile de gérer les erreurs de lecture. Toutes les fonctions de lecture retournent une valeur. Celle-ci peut prendre une valeur particulière qui signifie 'Arrêt de la lecture'. La cause n'est pas précisée. Ça peut être à cause d'une erreur (support en panne, données corrompu, fichier inexistant etc.) ou tout simplement par ce que la fin de fichier a été atteinte.

XXXIII-B-4-d-iv. Détection d'une erreur▲

La fonction fgets() retourne une valeur de type char *. Si la lecture a réussi, la valeur retournée est l'adresse du tableau de char passé en paramètre. En cas d'échec, la valeur NULL est retournée. Il suffit donc de surveiller cette valeur pour savoir si on peut continuer ou non. Comme une des causes d'échec est la "fin de fichier atteinte", on peut donc parfaitement intégrer ce test dans une boucle de lecture "ligne par ligne".

Une fois l'échec de la lecture constaté, il est possible d'en identifier la cause. Le langage C met à disposition les deux fonctions feof() et ferror() qu'il faut appeler après la boucle de lecture, mais avant la fermeture du fichier.

Sélectionnez
     while (fonction_de_lecture(fp) != ERREUR)
   {
      ...
   }

   if (feof(fp))
   {
      /* la fin de fichier a été détectée */
      puts ("EOF");
   }

   if (ferror(fp))
   {
      /* une erreur s'est produite */
      perror (NOM_DU_FICHIER);
   }

   fclose (fp);
XXXIII-B-4-d-v. Gestion des fins de ligne▲

On constate que lorsque fgets() lit une ligne entière, un '\n' se retrouve à la fin de la chaîne saisie. La présence de '\n' est gênante ou non selon l'application.

Ceci dit, dans tous les cas, il est conseillé d'en détecter la présence. En effet, sa présence indique que la ligne a été lue entièrement, alors que son absence indique que la ligne a été tronquée, et que d'autres caractères (au minimum un '\n') attendent pour être lus. Il est donc conseillé d'écrire ces quelques lignes après un fgets() pour clarifier la situation :

Sélectionnez
#include <stdio.h>
#include <string.h>
   ...
{
   char ligne[123];

   /* test d'erreur omis */
   fgets (ligne, sizeof ligne, fp);

   {
      /* chercher le '\n' */
      char *p = strchr(ligne, '\n');

      if (p != NULL)
      {
         /* si on l'a trouve, on l'elimine. */
         *p = 0;
      }
      else
      {
         /* Le traitement depend de l'application.
          * Par exemple, ici, on choisi d'ignorer
          * les autres caracteres.
          */

         /* sinon, on lit tous les caracteres restants */
         int c;

         while ((c = fgetc(fp)) != '\n' && c != EOF)
         {
         }
      }
   }
}

Il est clair que dans la pratique, l'ensemble de ce code devra être intégré dans une fonction unique de lecture d'une ligne à partir d'un flux.

XXXIII-B-4-d-vi. Exemple amélioré avec détection de la fin de lecture▲
Sélectionnez
/* fichier2.c */
#include <stdio.h>

int main (void)
{
   FILE *fp = fopen ("data.txt", "r");

   if (fp != NULL)
   {
      char ligne[32];

      /* définition d'un compteur de lignes et initialisation */
      int cpt = 0;

      /* lecture des lignes */
      while (fgets (ligne, sizeof ligne, fp) != NULL)
      {
         /* Mise a jour du compteur */
         cpt++;

         /* Affichage des lignes */
         printf ("%d: %s\n", cpt, ligne);
      }

      /* On peut ajouter ici la détection de la cause
       * de l'erreur décrite ci-dessus
       */

      fclose (fp);
   }
   else
   {
      printf ("Erreur d'ouverture du fichier\n");
   }

   return 0;
}

Cet exemple met en œuvre un mécanisme qui s'adapte automatiquement au nombre de lignes du fichier. Cependant, attention, le fonctionnement, bien qu'il reste sûr, risque d'être surprenant si la longueur de la ligne est supérieure à celle du tableau 'ligne'.

Par exemple, si on diminue la taille de 'ligne' à 16 au lieu de 32,

Sélectionnez
<...>
   char ligne[16];
<...>

on obtient :

Sélectionnez
1: Ceci est un sim
2: ple fichier

3: texte de 2 lign
4: es.
XXXIII-B-4-d-vii. Explication▲

Rappelons que la taille du tableau de char a été transmise à la fonction fgets().

Celle-ci tente de lire la ligne, mais celle-ci est trop longue pour tenir dans le variable 'ligne'. fgets(), qui connaît la taille de la variable 'ligne', applique alors une stratégie d'adaptation qui consiste à stocker ce qui est possible dans la variable, en laissant une place pour le 0 final. En effet, fgets() a pour obligation de produire une chaîne de caractères valide dans tous les cas.

C'est pourquoi la première ligne est partiellement lue ainsi :

Sélectionnez
0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15 : Indice
'C' 'e' 'c' 'i' ' ' 'e' 's' 't' ' ' 'u' 'n' ' ' 's' 'i' 'm'  0 : Données

Mais les caractères manquants ne sont pas perdus, et ils sont lus par l'appel suivant :

Sélectionnez
0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15 : Indice
'p' 'l' 'e' ' ' 'f' 'i' 'c' 'h' 'i' 'e' 'r' '\n' 0             : Données

Cette fois, la place est suffisante, et l'ensemble de la chaîné est lue, y compris le '\n'.

XXXIII-B-5. Écriture dans un fichier▲

Le langage C offre plusieurs fonctions permettant d'écrire des données dans un fichier :

  • fputc() ;
  • putc() ;
  • fwrite() ;
  • fprintf() ;
  • fputs().
XXXIII-B-5-a. fputc(), putc()▲

Ces fonctions sont identiques. Elles permettent d'écrire un caractère.

XXXIII-B-5-b. fwrite()▲

Cette fonction permet d'écrire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à l'écriture de données binaires brutes (non interprétées).

XXXIII-B-5-c. fprintf()▲

Cette fonction permet d'écrire des données 'texte' formatées. Elle comporte de nombreuses possibilités de conversion de valeurs numériques en texte. (Entiers, flottants etc.)

XXXIII-B-5-d. fputs()▲

Cette fonction permet d'écrire une chaîne de caractères.

XXXIII-B-6. Bien utiliser les formats de données▲

Il n'est pas rare que des données enregistrées dans un fichier par une machine soient lues par une autre machine, ou par autre programme ou par le même programme mais compilé avec des options différentes. Pour pouvoir récupérer les données, il faut qu'en aucun cas, le format des données enregistrées ne dépende de l'implémentation.

XXXIII-B-6-a. Format orienté texte▲

Le format texte est un bon choix, car il utilise une séquence de caractères simple et évidente (chronologique) et un codage très répandu (ASCII). Il peut y avoir quelques problèmes de transcodage pour les valeurs de 128 à 255 (ANSI, OEM etc.), mais rien qui ne soit insurmontable. D'autre part, la conversion ASCII/EBCDIC est triviale.

Il subsiste le problème des fins de ligne qui sont différentes d'un système à l'autre. Il existe des utilitaires bien connus (dos2unix, unix2dos etc.) généralement fournis avec ces systèmes qui font les conversions. Rappelons que la fonction system() permet d'appeler une commande extérieure. Si néanmoins, cet utilitaire n'existait pas, il serait facile de le faire soi-même. Bien sûr, il faudrait travailler en mode binaire de façon à contrôler les données du fichier de manière 'brute' (raw).

Les chaînes et les valeurs numériques sont encodées et éventuellement formatées avec fprintf(). Une organisation en ligne est souhaitable. Elles sont ensuite lues ligne par ligne avec fgets() et analysées soit par strtol(), strtoul() ou strtod() pour les cas les plus simples (valeurs numériques pures), soit par sscanf() pour les cas plus complexes, à condition que le formatage soit clairement défini. Il est souhaitable d'utiliser des formats simples à analyser et surtout sans ambiguïté quant aux séparateurs. Le format CSV est recommandé.

XXXIII-B-6-b. Format orienté binaire▲

Une mauvaise utilisation des formats binaires (raw) peut apporter des problèmes de portabilité. Il est recommandé d'utiliser des formats indépendants comme XDR (RFC 1832).

XXXIII-C. Supprimer un enregistrement dans un fichier binaire▲

Pour supprimer un enregistrement, le plus simple est de procéder ainsi :

  • le fichier original est ouvert en lecture. Un nouveau fichier est ouvert en écriture. L'original est lu enregistrement par enregistrement (fread()), et recopié dans le nouveau fichier (fwrite()) en omettant l'enregistrement à supprimer (if …) ;
  • par un jeu subtil de suppression et de renommage (remove(), rename()), on se retrouve avec une copie de l'original (genre .old ou .bak) et le nouveau fichier qui a maintenant le nom de l'ancien. L'opération reste simple, et a l'avantage de permettre l'annulation (par renommage de l'ancien fichier).

Toute autre opération basée sur l'écriture/lecture dans le même fichier est dangereuse, non portable et se traduit souvent par la destruction du fichier original sans recours possible.

XXXIII-D. En guise de conclusion▲

Il ne faut pas se tromper d'outil. Les flux du C sur disque sont très pratiques pour enregistrer quelques données statiques dans un fichier texte. En binaire, c'est déjà plus risqué à moins de passer par un format indépendant comme XDR. Pour gérer des enregistrements, les fichiers C sont trop rustiques. Il faut une véritable base de données (comme SQLite ou MySQL par exemple).

XXXIV. Pourquoi fflush (stdout) ?▲

Il arrive parfois de rencontrer ce genre de code…

Sélectionnez

printf("Entrez un nombre : ");
fflush (stdout);

… et on se demande alors à quoi peut bien servir ce fflush (stdout).

Le printf() précédent envoie une chaîne de caractères à stdout. Or cette chaîne n'est pas terminée par un '\n'.

Il faut savoir que stdout est souvent un flux "bufferisé", ce qui signifie, en bon français, que les caractères sont placés dans un tampon (buffer) de sortie avant d'être réellement émis.

Il y a trois critères qui déclenchent l'émission réelle des caractères :

  • le tampon d'émission est plein (incontrôlable) ;
  • un '\n' a été placé dans le tampon[1] ;
  • la commande de forçage a été activée.

La commande de forçage est activée par l'appel de la fonction fflush (stdout), ce qui explique sa présence dans le code mentionné.

[1] sauf en cas de redirection dudit flux vers un fichier.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XXXV. <time.h> : La gestion du temps
    • XXXV-A. Introduction
    • XXXV-B. Usage
      • XXXV-B-1. Lire l'heure courante
      • XXXV-B-2. Afficher l'heure courante
  • XXXVI. <time.h> : bien utiliser difftime()
  • XXXVII. rand(), srand()... j'y comprends rien...

XXXV. <time.h> : La gestion du temps▲

XXXV-A. Introduction▲

Le langage C fournit un certain nombre de fonctions permettant de lire l'heure/date courante et de convertir la valeur lue en structure ou en chaîne de caractères. Ces éléments sont définis et déclarés dans le header standard <time.h>

Élément | Nature | Description
struct tm | structure | Structure de date et heure
time() | fonction | Lecture de l'heure courante
time_t | type | Type retourné par time()
localtime() | fonction | Charge la structure tm avec l'heure locale
gmtime() | fonction | Charge la structure tm avec l'heure GMT
mktime() | fonction | Normalise la structure tm Retourne le temps en time_t
strftime() | fonction | Crée une chaîne formatée à partir d'une structure tm
difftime() | fonction | Calcule la différence entre deux dates

XXXV-B. Usage▲

XXXV-B-1. Lire l'heure courante▲

On utilise la fonction time(). On peut soit passer l'adresse d'un time_t, soit récupérer la valeur dans un time_t. Si on utilise pas l'adresse d'un time_t, il faut le préciser à la fonction en passant NULL :

Sélectionnez
#include <time.h>

int main (void)
{
   time_t ta;
   time_t tb;

   time (&ta);
   tb = time (NULL);

   return 0;
}

Telle quelle, la valeur contenue dans ta et tb n'a pas grande signification. La norme dit que c'est un grand entier qui représente un nombre d'élément de temps depuis une date donnée. Sous POSIX.1, c'est un nombre de secondes depuis le 1er janvier 1970. (Epoch). En principe, on ne manipule pas la valeur directement, mais via sa représentation en struct tm.

Cette valeur a quand même un intérêt pratique. Il y a de fortes chances qu'elle soit différente d'un lancement de programme à l'autre. On l'utilise donc couramment pour initialiser le générateur pseudo-aléatoire du C avec une valeur différente à chaque lancement :

Sélectionnez
srand((unsigned) time(NULL));

ce qui permet de démarrer le cycle de la séquence pseudo-aléatoire à un endroit difficilement prévisible, renforçant ainsi l'effet 'aléatoire' pour les jeux et autres programmes statistiques.

XXXV-B-2. Afficher l'heure courante▲

L'affichage de la date ou de l'heure de fait grace à la fonction strftime() qui a été conçue pour ça. Elle attend l'adresse d'une structure tm, qu'il va donc falloir mettre à jour au préalable. On utilise pour ça la valeur retournée par time(), qui donne la datation courante sous la forme d'un entier et une des fonction localtime() ou gmtime() selon que l'on veut convertir en heure locale ou en heure GMT (Greenwich). En France, il y a une heure de différence.

L'usage précis de strftime() et de ses nombreux paramètres de formatage doit être étudié dans un document de référence.

Sélectionnez
#include <stdio.h>
#include <time.h>

int main (void)
{
   /* lire l'heure courante */
   time_t now = time (NULL);

   /* la convertir en heure locale */
   struct tm tm_now = *localtime (&now);

   /* Créer une chaîne JJ/MM/AAAA HH:MM:SS */
   char s_now[sizeof "JJ/MM/AAAA HH:MM:SS"];

   strftime (s_now, sizeof s_now, "%d/%m/%Y %H:%M:%S", &tm_now);

   /* afficher le résultat : */
   printf ("'%s'\n", s_now);

   return 0;
}
Sélectionnez
'29/01/2009 02:34:00'

Process returned 0 (0x0)   execution time : 0.054 s
Press any key to continue.

XXXVI. <time.h> : bien utiliser difftime()▲

Les fonctions de <time.h> offrent une interface assez complexe. Voici un exemple qui rassemble l'usage de la plupart de ces fonctions.

Sélectionnez
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

int main (void)
{
   time_t now = time (NULL);
   struct tm tm_now = *localtime (&now);
   char s[64];

   strftime (s, sizeof s, "%d/%m/%Y", &tm_now);
   printf ("Aujourd'hui : %s\n", s);

   /* prochain Noel */
   {
      struct tm tm_xmas =
      {0};

      tm_xmas.tm_year = tm_now.tm_year;
      tm_xmas.tm_mon = 12 - 1;
      tm_xmas.tm_mday = 25;

      /* ajustement */
      {
         time_t xmas = mktime (&tm_xmas);

         strftime (s, sizeof s, "%d/%m/%Y", &tm_xmas);
         printf ("Noel : %s\n", s);

         {
            time_t diff = difftime (xmas, now);
            struct tm tm_diff = *gmtime (&diff);

            printf ("Plus que %d jours avant Noel\n", tm_diff.tm_yday);
         }
      }
   }

   return 0;
}

XXXVII. rand(), srand()... j'y comprends rien...▲

Un générateur pseudo-aléatoire est une machine qui génère une séquence de nombres déterminée, cyclique, mais difficile à prévoir pour un humain. De plus, la répartition des valeurs (histogramme) est supposée être équilibrée.

La génération n'est pas 'spontanée', mais 'à la demande' (par appel de la fonction rand()). Les valeurs produites sont comprises entre 0 et RAND_MAX inclus.

A chaque fois que l'on appelle rand(), une nouvelle valeur sort :

Sélectionnez
#include <stdio.h>
#include <stdlib.h>

int main (void)
{
   int i;
   printf ("Les valeurs vont de 0 a %d\n", RAND_MAX);
   for (i = 0; i < 10; i++)
   {
      int val = rand ();
      printf ("%d ", val);
   }
   printf ("\n");
   return 0;
}

Par exemple :

Sélectionnez
Les valeurs vont de 0 à 32767
41 18467 6334 26500 19169 15724 11478 29358 26962 24464

Mais on constate que si on lance le programme plusieurs fois, la séquence est toujours la même.

On peut modifier l'origine de la séquence avec srand(), en passant une valeur comprise entre 0 et RAND_MAX. Par exemple 10 :

Sélectionnez
#include <stdio.h>
#include <stdlib.h>

int main (void)
{
   int i;

   srand(10); /* MODIF */

   printf ("Les valeurs vont de 0 a %d\n", RAND_MAX);
   for (i = 0; i < 10; i++)
   {
      int val = rand ();
      printf ("%d ", val);
   }
   printf ("\n");
   return 0;
}

On constate que les valeurs sont différentes du tirage précédent, mais que si on relance le programme, elles restent identiques :

Sélectionnez
Les valeurs vont de 0 à 32767
71 16899 3272 13694 13697 18296 6722 3012 11726 1899

Pour avoir une séquence différente à chaque lancement du programme, il faut donc trouver un moyen de passer une valeur 'changeante' à srand(), d'où l'idée d'utiliser la valeur retournée par time(), qui change une fois par seconde, indépendamment du programme (c'est une valeur gérée par le système).

Une seconde, c'est long, mais ça suffit pour les besoins courants.

Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main (void)
{
   int i;

   srand((unsigned) time(NULL)); /* MODIF */

   printf ("Les valeurs vont de 0 a %d\n", RAND_MAX);
   for (i = 0; i < 10; i++)
   {
      int val = rand ();
      printf ("%d ", val);
   }
   printf ("\n");
   return 0;
}

Si on lance plusieurs fois le programme (au moins une seconde entre chaque lancement), on obtient maintenant des séquences différentes :

Sélectionnez
25985 16903 23861 29724 17917 23752 17039 25712 20507 30816
26197 27421 5384 20976 236 16846 22741 32047 12417 24408
26433 14874 13653 16830 21990 4658 17461 17892 21603 7731

etc. Je précise que pour que les valeurs changent dans le programme, il faut évidemment que srand() ne soit appelé qu'une seule fois au début du programme.

Après, il y a des astuces arithmétiques pour réduire la plage de valeurs... C'est du bête calcul entier... Détails dans la FAQ de f.c.l.c., notamment ici. D'autre part, ceci peut aider.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XXXVIII. Du bon usage de qsort()
    • XXXVIII-A. Description de la fonction qsort()
      • XXXVIII-A-1. Introduction
      • XXXVIII-A-2. Interface
        • XXXVIII-A-2-a. Interface de la fonction de comparaison
      • XXXVIII-A-3. Comportement
        • XXXVIII-A-3-a. Comportement de la fonction de comparaison
    • XXXVIII-B. Usage de la fonction qsort()
      • XXXVIII-B-1. Tri d'un tableau d'entiers
      • XXXVIII-B-2. Tri d'un tableau de chaînes
      • XXXVIII-B-3. Tri d'un tableau de structures
      • XXXVIII-B-4. Tri de nombres réels
  • XXXIX. Les identificateurs réservés
  • XL. Bien gérer la portée des objets et des fonctions
    • XL-A. Fonctions
    • XL-B. Objets
      • XL-B-1. Définition hors d'un bloc
      • XL-B-2. Définition dans un bloc
      • XL-B-3. Masquage (Shadowing)
  • XLI. Du bon usage de assert()
    • XLI-A. Exemple d'utilisation
  • XLII. Comportement indéfini
  • XLIII. Les item-lists
    • XLIII-A. Introduction
    • XLIII-B. Mise en oeuvre

XXXVIII. Du bon usage de qsort()▲

XXXVIII-A. Description de la fonction qsort()▲

XXXVIII-A-1. Introduction▲

La fonction qsort() implémente un algorithme de tri non spécifié qui permet de trier tout ou partie de n'importe quel tableau de données, du moment qu'il existe un critère de tri dans les données. Elle s'appuie sur une fonction utilisateur qui se charge d'exprimer le critère de tri.

C'est l'implémentation qui décide quel algorithme est utilisé. En général, l'algorithme est choisi pour sa performance. Il n'est pas rare que ce soit un Quick Sort.

XXXVIII-A-2. Interface▲

Le prototype de la fonction qsort() est

Sélectionnez
void qsort (void *tableau
            , size_t nb_elem
            , size_t taille_elem
            , int (*compare) (void const *a, void const *b));

Les paramètres sont :

  • void *tableau : adresse du premier élément du tableau à trier. La partie à trier doit être modifiable ;
  • size_t nb_elem : nombre d'éléments du tableau à trier (à ne pas confondre avec sa taille). Par exemple, tout le tableau : sizeof tab / sizeof *tab ;
  • size_t taille_elem : taille d'un élément du tableau : sizeof *tab ;
  • int (*compare) (void const *a, void const *b) : adresse de la fonction de comparaison (pointeur de fonction). Cette fonction est fournie par l'utilisateur.
XXXVIII-A-2-a. Interface de la fonction de comparaison▲

Cette fonction dispose de deux paramètres :

  • void const *a : adresse d'un des éléments du tableau en cours d'évaluation par l'algorithme. Il est qualifié 'const', car la fonction de comparaison ne doit en aucun cas en modifier le contenu sous peine de comportement indéfini ;
  • void const *b : adresse d'un autre élément du tableau en cours d'évaluation par l'algorithme. Il est qualifié 'const', car la fonction de comparaison ne doit en aucun cas en modifier le contenu sous peine de comportement indéfini.

de plus, elle doit retourner un int qui prend la valeur suivante (tri croissant) :

  • 0 si le critère de a est égal au critère de b ;
  • < 0 si le critère de a est inférieur au critère de b ;
  • > 0 si le critère de a est supérieur au critère de b.

XXXVIII-A-3. Comportement▲

La fonction qsort() effectue le tri du tableau selon le critère indiqué. La valeur retournée par la fonction de comparaison permet à l'algorithme de prendre les décisions qui s'imposent.

XXXVIII-A-3-a. Comportement de la fonction de comparaison▲

La fonction de comparaison reçoit l'adresse des deux éléments en cours d'évaluation. Par intermédiaire de pointeurs locaux typés et initialisés avec ces paramètres, elle doit évaluer le critère de tri et retourner un int de la valeur résultant de l'évaluation. Pour un entier, une simple différence suffit. Pour une chaîne, str[n]cmp() a été conçue pour ça. Pour un réel, c'est plus délicat, car la notion d'égalité est soumise à l'appréciation d'un EPSILON qui dépend de la précision recherchée.

XXXVIII-B. Usage de la fonction qsort()▲

Afin de dédramatiser l'usage de qsort(), qui fait parfois un peu peur, je fournis ici quelques exemples d'utilisation.

Attention, je suppose que les bases du C sont connues (tableaux, pointeurs, fonctions, I/O)

Ces exemples ne sont pas des suites de validation d'algorithmes de tri ! Ce ne sont que de simples exemples.

XXXVIII-B-1. Tri d'un tableau d'entiers▲

Soit un tableau de 5 entiers à trier : 4 , 6, -3, 4, 7,

Sélectionnez
#include <stdio.h>
#include <stdlib.h>

/* affichage du tableau */
static void aff (int *a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      printf ("=", a[i]);
   }
   printf ("\n");
}

/* fonction utilisateur de comparaison fournie a qsort() */
static int compare (void const *a, void const *b)
{
   /* définir des pointeurs typés et initialisés
      avec les parametres */
   int const *pa = a;
   int const *pb = b;

   /* évaluer et retourner l'état de l'évaluation (tri croissant) */
   return *pa - *pb;
}

int main (void)
{
/* tableau a trier */
   int tab[] = { 4, 6, -3, 4, 7 };

/* affichage du tableau dans l'état */
   aff (tab, sizeof tab / sizeof *tab);

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare);

/* affichage du tableau après le tri */
   aff (tab, sizeof tab / sizeof *tab);

   return 0;
}

Ce qui donne :

Sélectionnez
  4  6 -3  4  7
 -3  4  4  6  7

Press ENTER to continue.

XXXVIII-B-2. Tri d'un tableau de chaînes▲

Il s'agit maintenant de trier un tableau de chaînes constantes. Ici, il est particulièrement important de bien comprendre le sens de 'const'. Le tableau est modifiable, mais il est composé de pointeurs sur des chaînes non modifiables. Ce que va faire qsort(), c'est de réorganiser le tableau de pointeurs de façon à ce lorsqu'on lira le tableau en séquence, les chaînes pointées apparaissent comme 'triées'. Ce concept est assez subtil, car le tri est indirect. En effet, si on regardait la valeur des pointeurs dans le tableau, ils ne seraient probablement pas triés. De même, les chaînes en mémoire non modifiable sont, par définition, restées à leur place.

Dans la fonction de comparaison, comme d'habitude, on récupère les adresse des éléments en cours d'évaluation. Ici, les éléments sont de type char const *. Leur adresse est donc de type char const * *. Mais comme on a pas le droit de modifier les données pointées, on doit ajouter un 'const' pour être conforme aux paramètres : char const * const *

Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* affichage du tableau */
static void aff (char const **a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      printf ("%s\n", a[i]);
   }
   printf ("\n");
}

/* fonction utilisateur de comparaison fournie a qsort() */
static int compare (void const *a, void const *b)
{
   /* définir des pointeurs typés et initialisés
      avec les paramètres */
   char const *const *pa = a;
   char const *const *pb = b;

   /* évaluer et retourner l'état de l'évaluation (tri croissant) */
   return strcmp (*pa, *pb);
}

int main (void)
{
/* tableau a trier (tableau de pointeurs sur char const) */
   char const *tab[] = { "world", "hello", "wild" };

/* affichage du tableau dans l'état */
   aff (tab, sizeof tab / sizeof *tab);

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare);

/* affichage du tableau après le tri */
   aff (tab, sizeof tab / sizeof *tab);

   return 0;
}

malgré tout, le tri a eu lieu :

Sélectionnez
world
hello
wild

hello
wild
world

Press ENTER to continue.

XXXVIII-B-3. Tri d'un tableau de structures▲

Soit une structure comprenant {nom, prénom, age}. On crée un tableau de 5 éléments de ce type que l'on remplit "à la main", puis, on fait un tri par prénom, par age décroissant, puis par age croissant.

Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct fiche
{
   char nom[11];
   char prenom[11];
   int age;
};

/* affichage du tableau */
static void aff (struct fiche const *a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      /* pointeur intermédiaire pour alléger l'écriture */
      struct fiche const *p = a + i;
      printf ("%-10s %-10s %d ans\n", p->nom, p->prenom, p->age);
   }
   printf ("\n");
}

/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_prenom (void const *a, void const *b)
{
   /* définir des pointeurs typés et initialisés
      avec les paramètres */
   struct fiche const *pa = a;
   struct fiche const *pb = b;

   /* évaluer et retourner l'état de l'évaluation (tri croissant) */
   return strcmp (pa->prenom, pb->prenom);
}

/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_age (void const *a, void const *b)
{
   struct fiche const *pa = a;
   struct fiche const *pb = b;

   return pa->age - pb->age;
}

/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_age_dec (void const *a, void const *b)
{
   struct fiche const *pa = a;
   struct fiche const *pb = b;

   return pb->age - pa->age;
}

int main (void)
{
/* tableau à trier (tableau de pointeurs sur char const) */
   struct fiche tab[] = {
      {"Simpson", "Homer", 36},
      {"Bouvier", "Marge", 34},
      {"Simpson", "Bart", 10},
      {"Simpson", "Lisa", 8},
      {"Simpson", "Maggie", 2},
   };

/* affichage du tableau dans l'état */
   aff (tab, sizeof tab / sizeof *tab);

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_prenom);

/* affichage du tableau après le tri */
   aff (tab, sizeof tab / sizeof *tab);

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_age);

/* affichage du tableau après le tri */
   aff (tab, sizeof tab / sizeof *tab);

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_age_dec);

/* affichage du tableau après le tri */
   aff (tab, sizeof tab / sizeof *tab);

   return 0;
}
Sélectionnez
Simpson    Homer      36 ans
Bouvier    Marge      34 ans
Simpson    Bart       10 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans

Simpson    Bart       10 ans
Simpson    Homer      36 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans
Bouvier    Marge      34 ans

Simpson    Maggie     2 ans
Simpson    Lisa       8 ans
Simpson    Bart       10 ans
Bouvier    Marge      34 ans
Simpson    Homer      36 ans

Simpson    Homer      36 ans
Bouvier    Marge      34 ans
Simpson    Bart       10 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans

Press ENTER to continue.

XXXVIII-B-4. Tri de nombres réels▲

Pour trier les nombres réels (float, double), il y a quelques précautions à prendre, car la notion d'égalité n'existe pas. Il faut travailler par plages :

Sélectionnez
#include <stdio.h>
#include <time.h>

double random_range (double min, double max)
{
   return min + ((max - min) * (rand () / (double) RAND_MAX));
}

static int cmp (void const *a, void const *b)
{
   int ret = 0;
   double const *pa = a;
   double const *pb = b;
   double diff = *pa - *pb;
   if (diff > 0)
   {
      ret = 1;
   }
   else if (diff < 0)
   {
      ret = -1;
   }
   else
   {
      ret = 0;
   }

   return ret;
}

int main (void)
{
   double tab[100];
   int i;
   srand ((int) time (NULL));
   for (i = 0; i < sizeof tab / sizeof *tab; i++)
   {
      tab[i] = random_range (0, 1);
   }

   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, cmp);

   for (i = 0; i < sizeof tab / sizeof *tab; i++)
   {
      printf ("%8.4f", tab[i]);
   }

   return 0;
}

XXXIX. Les identificateurs réservés▲

Le document de définition du langage C a réservé un certain nombre d'identificateurs réservés. Ils peuvent être utilisés par les implémenteurs (ceux qui écrivent les compilateurs) ou pour des extensions du langage.

Ces identificateurs peuvent apparaître dans les interfaces publiques ou pour réaliser certaines parties des fichiers d'entête (macros, paramètres...). Ils sont choisis de façon à ne pas interférer avec les identificateurs des utilisateurs, sous réserve, bien sûr, que ceux-ci ne les utilisent pas, d'où l'intérêt de cet article.

Les identificateurs réservés sont :

Sélectionnez
+---------------------+----------------+---------------------+----------------+
 | Les identificateurs |                | Exemple             | Exemple        |
 | commençant par      | suivi de       | valide              | Réservé        |
 +---------------------+----------------+---------------------+----------------+
 | "_"                 | "_A-Z"         | _123                | _ABC           |
 +---------------------+----------------+---------------------+----------------+
 | "is"                | "a-z"          | is_abc              | isabc          |
 +---------------------+----------------+---------------------+----------------+
 | "mem"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "str"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "to"                | "a-z           |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "wcs"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "E"                 | "A-Z" ou "0-9" | Eabc                | E123 EABC      |
 +---------------------+----------------+---------------------+----------------+
 | "LC_"               | "A-Z"          | LC_abc LC_123 LCABC | LC_ABC         |
 +---------------------+----------------+---------------------+----------------+
 | "SIG"               | "_A-Z"         | SIGabc              | SIGABC SIG_ABC |
 +---------------------+----------------+---------------------+----------------+

XL. Bien gérer la portée des objets et des fonctions▲

Le langage C offre par nature un contrôle assez fin de la portée des objets et des fonctions. Cette caractéristique est souvent mal connue, pourtant elle apporte un bénéfice certain, notamment sur le plan de l'organisation du code (conception détaillée).

XL-A. Fonctions▲

Par défaut, la portée d'une fonction est globale.

Sélectionnez
int function (int a, char *b)
{
}

Elle est visible d'un autre module une simple déclaration :

Sélectionnez
int function ();

ou mieux, un prototype :

Sélectionnez
int function (int a, char *b);

Il est possible cependant de réduire la portée de la fonction à l'unité de compilation dans laquelle elle a été définie, en ajoutant le qualificateur static.

Sélectionnez
static int function (int a, char *b)
{
}

Cette pratique, lorsqu'elle est possible, apporte différents avantages :

  • une économie d'identificateurs. La portée de celui-ci étant limitée à une unité de compilation, il est possible de le réutiliser pour une autre fonction qualifiée 'static' dans une autre unité de compilation ;
  • une meilleure optimisation. Certains compilateurs sont capables d'"inliner" une telle fonction dans certaines conditions, ce qui diminue le temps d'exécution au prix d'une augmentation de la taille (le code de la fonction est recopié autant de fois que nécessaire) ;
  • une meilleure organisation du code. Etant donné que ces fonctions sont forcément appelées par une fonction de l'unité de compilation dans laquelle elles ont été définies, le code se trouve naturellement organisé en blocs fonctionnels cohérents. De plus, comme à priori, ces fonctions n'ont pas besoin de prototypes séparés, cela favorise une organisation du fichier source selon le principe 'Définir avant d'utiliser' (Top-down)

On évitera cependant de multiplier les codes identiques, et les principes de factorisation du code restent en vigueur.

Sélectionnez
+--------------------+    +--------------------+
| Bloc fonctionnel A |    | Bloc Fonctionnel B |
+--------------------+    +--------------------+
               |               |
               v               v
             +--------------------+
             |       Outils       |
             +--------------------+

XL-B. Objets▲

La portée d'un objet est régie selon plusieurs critères.

XL-B-1. Définition hors d'un bloc▲

La portée par défaut est globale. Elle peut être réduite à l'unité de compilation en ajoutant le qualificateur static.

XL-B-2. Définition dans un bloc▲

Si deux objets ont le même nom, l'objet de portée inférieure masque les objets de portée supérieure. Pour cette raison, qui entraîne un comportement confus, on évite de donner le même nom à des objets dont les portées sont imbriquées.

La portée est celle du bloc et des blocs inclus.

XL-B-3. Masquage (Shadowing)▲

Sélectionnez
/* objet de portée globale */
int x;

int f (void)
{
   /* la globale x est masquée par une locale du même nom */
   int x = 0;

   /* la globale x n'est pas modifiée. */
   x++;
}

int main (void)
{
   /* la globale x est modifiée */
   x = 2;

   f();

   /* la globale x vaut toujours 2 */

   return 0;
}

XLI. Du bon usage de assert()▲

assert() est une macro qui permet de 'poser un piège'. On s'en sert en phase de mise au point pour vérifier si la conception et la réalisation sont correctes.

Le paramètre est une expression. Si elle retourne 0 (expression fausse), le programme s'arrête et un message indiquant le lieu et la cause est affiché.

Il est d'usage qu'en mode production (release), la macro globale NDEBUG soit définie, ce qui fait que les macros assert(), bien que toujours présentes dans le source, ne génèrent plus aucun code de vérification. En conséquence, cette macro ne doit évidemment pas être utilisée pour détecter des erreurs d'utilisation ou de système.

XLI-A. Exemple d'utilisation▲

Sélectionnez
#include <stdio.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      printf ("M", t[i]);
   }
   printf ("\n");
}

int main (void)
{
   int tab[] = {1, 2, 3, 4};

   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

Ce code parait correct, mais à l'exécution, on constate :

Sélectionnez
1   2   3   4   2

Press ENTER to continue.

Pour vérifier le comportement, je pose un piège qui vérifie la validité de l'index :

  • il doit être >=0 (toujours vrai, vu le type size_t) ;
  • il doit être < n.

Je vais donc ajouter un piège :

Sélectionnez
assert (i < n);

Avant l'accès en lecture au tableau.

Pour être valide, la conception du piège doit se faire sans lire le code à tester, mais en se basant uniquement sur l'interface et le comportement présumé.

Sélectionnez
#include <stdio.h>
#include <assert.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      /* ajout du piège */
      assert (i < n);
      printf ("M", t[i]);
   }
   printf ("\n");
}

int main (void)
{
   int tab[] = {1, 2, 3, 4};

   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

Ce qui provoque bien sûr :

Sélectionnez
1   2   3   4Assertion failed: i < n, file main.c, line 10

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

Press ENTER to continue.

Ce qui signifie que i a dépassé la valeur maximale qu'autorise le langage C. La cause est évidemment le <= au lieu de < dans l'expression du for(), ce qui entraîne une action corrective immédiate :

Sélectionnez
#include <stdio.h>
#include <assert.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   /* correction */
   for (i = 0; i < n; i++)
   {
      /* ajout du piège */
      assert (i < n);
      printf ("M", t[i]);
   }
   printf ("\n");
}

int main (void)
{
   int tab[] = {1, 2, 3, 4};

   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

L'exécution et, à présent, conforme aux attentes :

Sélectionnez
1   2   3   4

Press ENTER to continue.

XLII. Comportement indéfini▲

Le langage C est défini par un document unique et reconnu sur le plan international (ISO) par tous les intervenants, que ce soit les développeurs de compilateurs (les 'implémenteurs') les développeurs d'applications (les 'utilisateurs') ou les différents formateurs.

Ce document de référence définit un certain nombre d'éléments (obligations, interdictions).

Les autres éléments sont soit laissés à l'appréciation des implémenteurs (implementation defined ou défini par l'implémentation) qui doivent accompagner leur production (compilateur etc.) d'un document précisant les comportement de tel ou tels éléments, soit non définis du tout. Dans ce dernier cas, le comportement est dit indéfini ou indéterminé. (Undefined Behaviour ou UB)

Quelques exemples :

Sélectionnez
#include <stdio.h>

int main (void)
{
   int i = 0;

   printf ("i = %d\n", i);
   return 0;
}

Ce code est conforme à la spécification du langage, aucune zone n'a été laissée dans l'ombre. Le comportement est déterminé. Il est garanti d'écrire

Sélectionnez
i = 0

Par contre, voici deux cas de comportement indéterminé :

  • Absence de prototype pour printf()
Sélectionnez
int main (void)
{
   int i = 0;

   printf ("i = %d\n", i);
   return 0;
}
  • Lecture d'une valeur non initialisée
Sélectionnez
#include <stdio.h>

int main (void)
{
   int i;

   printf ("i = %d\n", i);
   return 0;
}

Les conséquences d'un UB ne sont pas prévisibles. En effet, ça va du crash au comportement d'apparence conforme. Il est donc impossible de compter sur la simple vérification du comportement pour garantir qu'un code est correct. Il faut avant tout qu'il soit exempt de tout UB.

Le compilateur et ses warnings (ou un outil d'analyse spécialisé comme Lint) peut nous aider à débusquer certains UB. Ici, il est probable qu'une 'utilisation de variable non initialisée' ou qu'un 'appel de fonction sans prototypes' soient détectés (mais ça dépend du compilateur et de ses réglages). Mais il est des cas où le compilateur ne voit rien. Le seul recours est alors l'œil exercé du programmeur expérimenté.

La chasse aux UB est donc ouverte en permanence. C'est la principale source de bugs dans un programme C. Il convient donc, d'une part, de bien connaître le langage et ses limites de définition et, d'autre part, d'être extrêmement vigilant lors de l'écriture et de la relecture du code. Lorsqu'elle est possible, la relecture croisée est une bonne méthode de détection des UB.

Exercice : trouver le UB :

Sélectionnez
#include <stdio.h>

int main (void)
{
   int num = 12;
   char num_text[] = "";

   sprintf (num_text, "%d", num);
   printf ("Voici num_text : %s\n", num_text);
   return 0;
}

XLIII. Les item-lists▲

XLIII-A. Introduction▲

Qui n'a jamais été confronté à ce problème :

Comment faire le lien entre des constantes symboliques et leur représentation textuelle ?

Le but des item-lists est de résoudre de façon la plus automatique possible ce genre de problème.

XLIII-B. Mise en oeuvre▲

Le principe est de séparer les informations de bases constantes (par exemple, la correspondance entre caractère et chaîne morse) dans un fichier indépendant (ni.c, ni.h, on y reviendra), mais que l'on peut inclure (#include) de façon à réaliser une génération automatique de code en fonction de la demande.

Je prends un exemple plus parlant.

Je veux créer une série de constantes qui représentent des fruits

Sélectionnez
enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI };

Ca peut servir à définir une masse de fruit :

Sélectionnez
struct dosage_fruit
{
   enum fruits fruit;
   int masse;
};

puis des recettes de fruits mixés :

Sélectionnez
struct dosage_fruit mix_energie[] =
{
   {BANANE, 100},
   {ORANGE, 150},
   {FRAISE, 80},
};

struct dosage_fruit mix_forme[] =
{
   {BANANE, 50},
   {ORANGE, 50},
   {POMME, 100},
   {KIWI, 50},
   {FRAISE, 50},
};

// etc.

Si on veut afficher la recette, il faut un moyen simple pour convertir la constante fruit en une chaîne imprimable.

On va donc utiliser un tableau de chaînes construit sur le même modèle que le enum (même ordre, c'est primordial, car l'enum sert d'indice au tableau):

Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
};

ce qui permet maintenant d'afficher la composition :

Sélectionnez
void afficher_composition (char const *nom, struct dosage_fruit const *a,
                           size_t n)
{
   size_t i;
   printf ("%s\n", nom);
   for (i = 0; i &lt; n; i++)
   {
      struct dosage_fruit const *p = a + i;
      printf ("%d g de %s\n", p->masse, chaines_fruits[p->fruit]);
   }
   printf ("\n");
}

que l'on appelle comme ceci :

Sélectionnez
#define N(a) (sizeof(a) / sizeof *(a))

{
   afficher_composition ("Mix energie", mix_energie, N(mix_energie));
   afficher_composition ("Mix forme", mix_forme, N(mix_forme));

   etc.

Ce qui donne

Sélectionnez
Mix energie
100 g de banane
150 g de orange
80 g de fraise

Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise

Press ENTER to continue.

(je laisse au programmeur malin le soin d'écrire «d'orange» au lieu de «de orange»…)

Maintenant, patatras, nouvelle recette à la carte :

Sélectionnez
struct dosage_fruit mix_tropical[] =
{
   {BANANE, 50},
   {ORANGE, 80},
   {MANGUE, 100},
   {ANANAS, 50},
};

Quelles sont les conséquences sur le programme :

Deux modifications :

  • l'enum :
    Sélectionnez
    enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };
  • la liste des chaînes 'associées' (manuellement, pour le moment) :
Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

Si j'inverse ou que j'en oubli une, c'est la catastrophe. Idem, et c'est beaucoup plus sournois, si j'oublie une ','.

Donc, après quelques sueurs froides, ça marche, et on obtient bien :

Sélectionnez
Mix energie
100 g de banane
150 g de orange
80 g de fraise

Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise

Mix tropical
50 g de banane
80 g de orange
100 g de mangue
50 g de ananas

Press ENTER to continue.

Mais c'est déjà beaucoup de stress dans un petit programme comme ça. Dans l'industrie, la moyenne, c'est 1 000 000 de lignes… Pas question de se stresser comme ça si, pour ajouter une valeur dans la liste, il faut modifier dans 10 fichiers différents… (Lesquels ? On n'est pas des robots…)

Par contre, on peut utiliser des techniques de programmations qui font que la maintenance est centralisée en un seul fichier. On le modifie, on recompile tout le projet, et les modifications sont automatiquement reportées dans tout le code.

Pour ça, on va faire travailler la machine à partir d'un fichier unique, pour qu'elle produise ce qu'on veut. C'est toute la puissance qu'offre le préprocesseur bien maîtrisé.

Je vais montrer les différentes étapes pour expliquer le principe, mais dans la pratique, on ne fait que la dernière évidemment.

Dans notre exemple, Les deux éléments à "synchroniser" sont :

Sélectionnez
enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };

et la liste des chaînes 'associées' (manuellement, pour le moment)

Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

Si on observe bien ces deux éléments, on constate qu'ils se ressemblent. Pour être plus parlant, on va les réorganiser en colonnes :

Sélectionnez
enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS
};

et la liste des chaînes 'associées' (manuellement, pour le moment)

Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

On voit que le schéma est similaire :

  • une entête ;
  • une liste (chaque élément est terminé par une ,) ;
  • une fin particulière.

On voit que la liste des enum présente une petite irrégularité : le dernier élément n'est pas terminé par une ','. C'est normal (et obligatoire) en C90 (en C99, la virgule est acceptée).

Petite astuce : on ajoute un élément à la liste, qui sert à terminer la liste sans virgule. Il ne fait pas partie de la liste. C'est soit un 'dummy' (inutile), soit, comme ici où les valeurs sont automatiques, une constante qui exprime le nombre d'éléments de la liste, ce qui, à priori, n'est pas complètement inutile…

Modification :

Sélectionnez
enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS,
   NB_FRUITS
};

Afin de faciliter la maintenance, il faudrait disposer d'une liste 'double' comme ceci :

Sélectionnez
BANANE "banane"
ORANGE "orange"
POMME  "pomme"
FRAISE "fraise"
KIWI   "kiwi"
MANGUE "mangue"
ANANAS "ananas"

Comment faire comprendre ça à un programme C ?

C'est la qu'intervient la puissance du préprocesseur.

Il suffit d'écrire une liste de macros avec deux paramètres. Chaque macro représentant un groupe cohérent d'informations ou 'item' :

Sélectionnez
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

Il suffit ensuite d'écrire la définition de ITEM qui correspond à l'usage qu'on en fait :

Sélectionnez
enum fruits
{
#define ITEM(id, chaine)\
   id,

ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

#undef ITEM

   NB_FRUITS
};

Ce qui va produire automatiquement la bonne liste.

On fait pareil avec l'autre liste :

Sélectionnez
static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,

ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

#undef ITEM
};

Le #undef permet le 'recyclage' du nom de la macro qui est invariablement ITEM.

L'étape ultime consiste à inclure la liste à partir d'un fichier extérieur auquel je donne l'extension .itm (par exemple : fruits.itm semble approprié).

Je conseille de placer un commentaire dans ce fichier qui rappelle son nom et le début de la définition de la macro avec la signification des champs.

Sélectionnez
/* fruits.itm

#define ITEM(id, chaine)\

*/
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

La maintenance de ce fichier est extrêmement simple. Elle est «visuelle».

Les deux définitions deviennent alors :

Sélectionnez
enum fruits
{
#define ITEM(id, chaine)\
   id,
#include "fruits.itm"
#undef ITEM

   NB_FRUITS
};

et :

Sélectionnez
static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,
#include "fruits.itm"
#undef ITEM
};

Ce qui allège considérablement le code source et rend la maintenance automatique. (Le C, c'est Bien)

Il peut y avoir des centaines de lignes dans un .itm. Idem pour le nombre de champs. Ici, il y en a deux champs, mais on aurait pu en avoir qu'un seul. En effet, une macro sait transformer un paramètre symbolique (ORANGE) en une chaîne ("ORANGE") avec #. Mais elle ne sait pas modifier la casse des caractères, d'où mon choix de mettre deux champs.

Exemples de fichiers itm que j'utilise

(Simple) : gestion des erreurs

(Complexe) : tables de caractères

Je laisse au lecteur le soin d'écrire l'ensemble de l'exemple et de faire les tests nécessaires. Tous les éléments sont là.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Notes sur le langage C

Table des matières

  • XLIV. Borland C : "floating point formats not linked"
  • XLV. Code standard ? Code portable ? Je suis perdu !
    • XLV-A. "standard" ?
    • XLV-B. "portable" ?
      • XLV-B-1. portabilité absolue
      • XLV-B-2. portabilité relative
    • XLV-C. Bon usage
  • XLVI. Enregistrer une structure
    • XLVI-A. Le format binaire
    • XLVI-B. Le format texte

XLIV. Borland C : "floating point formats not linked"▲

Un programme généré avec l'IDE Borland C++ 3.1 signale parfois ce message à l'exécution :

Sélectionnez
scanf : floating point formats not linked
Abnormal program termination

Il s'agit en fait d'un bug connu de certains compilateurs Borland. Il se produit lorsqu'on utilise un format 'flottant' avec *printf() ou *scanf(), et qu'on n'utilise pas de fonction de la bibliothèque mathématique.

La parade est simple. Il suffit d'ajouter ces quelques lignes dans le code source contenant le main(), par exemple.

Sélectionnez
#ifdef __BORLANDC__
/* The pesky "floating point formats not linked" killer hack : */
extern unsigned _floatconvert;
#pragma extref _floatconvert
#endif

XLV. Code standard ? Code portable ? Je suis perdu !▲

On entend parler de code standard, de code portable ? Qu'est-ce que ça signifie ? Cela sert à quoi ?

XLV-A. "standard" ?▲

Le terme 'standard' est erroné. C'est un anglicisme de 'standard' qui signifie 'norme' (subst.) ou 'normalisé' (adj.).

Le langage C est normalisé. Cela signifie en clair qu'il est défini par un document spécifié et publié sous la responsabilité de l'ISO (International Standard Organisation ou Organisme de normalisation international). Ce document décrit la syntaxe et la sémantique du langage ainsi que l'interface et le comportement des fonctions de la bibliothèque d'exécution (RTL ou Run-Time Library).

Cette spécification s'applique aux compilateurs réputés 'conformes à la norme' et par conséquent aux programmeurs qui les utilise. La spécification définit en gros trois domaines :

  • ce qui est défini par la norme ;
  • ce qui est défini par la cible[1] ;
  • ce qui n'est pas défini.

Ce qui n'est pas défini par la norme peut l'être par une implémentation du C qui comporte des extensions spécifiques à une cible ou à un système. (Mots clés, fonctions, bibliothèques)

Par exemple system() est une fonction normalisée, dont le paramètre est une chaîne de caractères. Cependant, la sémantique du texte porté par cette chaîne de caractères peut varier d'un système à l'autre, voire ne pas être reconnue du tout par le système.

[1] (on dit aussi implémentation (anglicisme), implantation ou plate-forme)

XLV-B. "portable" ?▲

C'est la capacité qu'a un code source à produire un comportement identique sur différentes plate-formes. On distingue la portabilité absolue (pour n'importe quelle plate-forme) de la portabilité relative (limitée à un certain nombre de plate-formes bien définies).

XLV-B-1. portabilité absolue▲

C'est lorsqu'un code source ne contient que des éléments normalisés du langage dont la définition et l'utilisation ne dépendent pas de la plate-forme. Certaines pratiques additionnelles peuvent cependant rendre portable du code standard, comme ajouter fflush(stdout) après un printf() qui ne se termine pas par un '\n'.

XLV-B-2. portabilité relative▲

C'est un code portable 'absolu' auquel s'ajoutent des extensions (généralement, des bibliothèques) tierces conçues pour fonctionner sur un certain nombre de plate-formes bien définies.

La norme POSIX.1 en définit un certain nombre, notamment en matière de gestion des répertoires, processus légers (threads) et réseau (sockets). Mais il existe des initiatives indépendantes comme GTK+ qui définit une interface de programmation graphique pour utilisateur (GUI) commune à Windows, X (Unix/Linux), Apple/Mac et même BeOS.

XLV-C. Bon usage▲

Le fait de bien connaître les domaines couverts par le langage C constitue une aide considérable pour l'écriture du code. En effet, la portabilité n'est possible que si le code est portable et donc, dans sa grande majorité normalisé, ou tout au moins conforme aux définitions de telle ou telle bibliothèque d'abstraction.

Il est donc impératif de séparer le code normalisé (qui doit représenter la majorité de celui-ci) du code spécifique à telle ou telle plate-forme, et qui n'aurait pas trouvé sa place dans les bibliothèques d'abstraction.

XLVI. Enregistrer une structure▲

L'enregistrement d'une structure dans un fichier est une opération plus complexe qu'il n'y parait. En effet, le code naif suivant :

Sélectionnez
#include <stdio.h>

struct data
{
   char nom[32];
   int age;
};

int main (void)
{
#define FNAME "data.txt"

   struct data data = { "Emmanuel", 50 };

   FILE *fp = fopen (FNAME, "wb");
   if (fp != NULL)
   {
      fwrite (&data, sizeof data, 1, fp);

      fclose (fp), fp = NULL;
   }
   else
   {
      perror (FNAME);
   }
   return 0;
}

est certes simple et efficace, mais est malheureusement non portable, et ce pour plusieurs raisons :

  • la représentation interne des données en C peut changer d'une implémentation à l'autre, en terme de largeur (nombre de bits), de 'boutisme' (position du byte de poids fort) et de codage (entiers négatifs, nombres réel, jeu de caractères) ;
  • l'alignement requis. En effet, certaines architectures imposent que les éléments de la structure soient alignés sur une adresse multiple de 2, 4 ou autre, ce qui rend impossible de prévoir de façon portable, la signification des bytes dans le fichier.

Pour résoudre ce problème, il y a deux grandes familles de solutions :

  • le format binaire ;
  • le format texte.

XLVI-A. Le format binaire▲

C'est le plus portable, mais aussi le plus complexe. Il consiste à définir un format de données indépendant de toute implémentation. Il nécessite une conversion à l'écriture (host->file) et une conversion à la lecture (file->host), et ce dans le strict respect du format 'fichier' spécifié. Une méthode simple est TLV (Type, Longueur, Valeur ou Type, Length, Value).

(à venir : exemple de spécification TLV)

Il existe des solutions normalisées comme BER (Basic Encoding Rules) spécifié par les recommandations ITU-T X.209 et X.690 ou XDR (eXternal Data Representation) spécifié par la RFC 1832 plus ou moins basées sur TLV. Ces solutions sont complexes et sont plutôt utilisées avec l'aide d'une bibliothèque tierce comme BER sous Linux.

Dans tous les cas, le fichier est traité en mode binaire ("wb", "rb", "ab"), et les fonctions les plus utilisées sont fgetc(), fputc(), fread() et fwrite().

XLVI-B. Le format texte▲

Il est un peu moins portable, mais moins complexe. Il consiste à définir un format de données indépendant sous forme de texte. Il nécessite une conversion à l'écriture (host->file) et une conversion à la lecture (file->host), et ce dans le strict respect du format 'fichier' spécifié. Une méthode simple est CSV (Comma Separated Values).

On peut trouver les spécifications sur Wotsit , le site de référence des formats de fichiers.

Le fichier est traité le plus souvent en mode texte ("w", "r", "a"), mais aussi parfois en mode binaire pour régler les problèmes de fins de ligne hétérogènes. (voir plus loin). Les fonctions les plus utilisées sont fgetc(), fputc(), fgets(), fputs() et fprintf().

Les principaux problèmes de portabilité proviennent :

  • du jeu de caractères utilisé. En dehors d'EBCDIC utilisé sur les gros systèmes IBM (Mainframes), c'est généralement ASCII (0-127), mais ça peut ne pas suffire à encoder tous les caractères, notamment les caractères accentués et autres signes mathématiques ou pseudo-graphiques. Or il existe plusieurs façons de coder ces caractères (ASCII étendu, IBM-PC8, UTF-8, Unicode, etc.). Là encore, une définition indépendante peut aider… ;
  • de la façon de coder les fins de ligne (CR, LF, CRLF, etc.).

Certaines corrections doivent donc parfois être effectuées à l'aide de tables de codages et autres astuces algorithmiques.

Liste des articles d'Emmanuel Delahaye

  • Initiation Pragmatique au langage C
  • Bonnes pratiques de codage en C
  • Bibliothèque CLIB
  • Notes sur le langage C
  • Concevoir et réaliser un composant logiciel en C
  • Les types abstraits de données (ADT)
  • Notes sur les réseaux
  • L'outil MAKE
  • POSIX threads
  • Les listes chaînées en langage C
  • Entrées solides en langage C
  • Introduction au développement d'un projet informatique
  • Opérateurs bit à bit

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com